Feature Flag Service: Overrides, Rollouts and Targeting
EasyAI-assisted coding · AI-Assisted Feature Work
Feature Flag Service: Overrides, Rollouts and Targeting
You've joined the platform team that owns flagsvc, the small service every product team calls to ask "is this feature on for this user?". Support has reported a few bugs in per-user overrides, and two features are queued behind them: gradual percentage rollouts and segment targeting. Everything runs in memory, and run handles one batch of operations against a fresh service.
Codebase
| File | What it does |
|---|---|
flagsvc/api.py | run(operations) entry point and the Dispatcher that routes each operation |
flagsvc/service.py | FlagService: validates arguments and coordinates everything below |
flagsvc/evaluator.py | Evaluator: runs the decision steps in precedence order |
flagsvc/overrides.py | OverrideTable: per-user pinned values |
flagsvc/rollout.py | rollout_decision: percentage rollout step (currently has no opinion) |
flagsvc/hashing.py | bucket(flag, user_id): stable user bucketing (stub) |
flagsvc/targeting.py | targeting_decision: segment-rule step (currently has no opinion) |
flagsvc/store.py | FlagStore: registry of flags and segments |
flagsvc/models.py | Flag, Rule, Segment and EvalContext dataclasses |
flagsvc/validation.py | Shared argument checks |
flagsvc/config.py, flagsvc/errors.py | Constants and error codes |
tests/ | Existing unit and end-to-end tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh service and returns one result per operation. Errors are returned as strings, never raised.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
define | flag, default | true | invalid arguments, flag exists |
set_override | flag, user_id, value | true | unknown flag, invalid value |
clear_override | flag, user_id | true if an override was removed, else false | unknown flag |
evaluate | flag, user_id or flag, user_id, attrs | true / false | unknown flag |
set_rollout *(Part 2)* | flag, percent | true | unknown flag, invalid percent |
define_segment *(Part 3)* | name, conditions | true | invalid arguments, invalid condition |
add_rule *(Part 3)* | flag, segment, value | true | unknown flag, unknown segment, invalid value |
- Each error is returned as
"error: <code>", e.g."error: unknown flag". - An unknown operation name returns
"error: unknown operation". A wrong number of arguments, a malformed flag or segment name, an empty or non-stringuser_id, a non-booleandefault, or anattrsthat is not an object returns"error: invalid arguments". Names are lowercase letters, digits,.,-and_(seeconfig.NAME_PATTERN). User ids are case-sensitive. set_rollout,define_segmentandadd_ruleexist in the dispatcher but currently answer"error: not supported".
Worked example
[["define", "new-checkout", false],
["evaluate", "new-checkout", "u1"],
["set_override", "new-checkout", "u1", true],
["evaluate", "new-checkout", "u1"],
["evaluate", "new-checkout", "u2"],
["evaluate", "dark-mode", "u1"]]returns
[true, false, true, true, false, "error: unknown flag"]Part 1 — Fix the override bugs (PLAT-512, PLAT-518, PLAT-525)
Goal: per-user overrides behave exactly as documented below.
Reports:
- "We pinned
banneroff for one account on a flag whose default is on, and that account still sees the banner." - "
set_overrideaccepted1as the value, and a script that sent0was happily accepted too." - "
clear_overridesaystrueeven when I never set an override for that user."
Rules
- An override is a JSON boolean. Anything else (numbers, strings,
null, lists) returnsinvalid value, and nothing is stored. - An override of
falseis an explicit decision: it takes precedence over the flag's default exactly like an override oftruedoes. Overriding the same user again replaces the old value. - Overrides belong to one (flag, user) pair; they never affect other users or other flags.
clear_overridereturnstrueif it removed an override andfalseif there was none. After clearing, the user is evaluated as if no override had ever been set.- Check order: the flag must exist (
unknown flag) before the value is checked.
Part 2 — Percentage rollouts (PLAT-540)
Goal: set_rollout switches a flag on for a stable percentage of users.
A flag may have a rollout percentage. A user is in the rollout when their bucket is below the percentage's threshold; users in the rollout get true, everyone else falls through to the flag's default. (Rollouts never switch anyone off.)
Rules
- A user's bucket is
int(hex[:8], 16) % 10000, wherehexis the hexadecimal SHA-256 digest of the UTF-8 string"<flag>:<user_id>". Usehashlib; Python's built-inhash()changes between processes.flagsvc/hashing.pyis where this belongs. percentis an integer or float from0to100inclusive; booleans, strings and out-of-range numbers returninvalid percent.nullremoves the rollout and returnstrue. The threshold isround(percent * 100), and a user is in the rollout whenbucket < threshold(strictly). So0includes nobody,100includes everybody, and12.5covers buckets0..1249.- Calling
set_rolloutagain replaces the previous percentage. Raising a percentage never removes a user who was already in. - A user override (Part 1) still wins over the rollout, in both directions.
- Evaluation order is override, then rollout, then default.
Example. With new-checkout defined with default false, the buckets of u0, u4, u7 and u8 are 5510, 2686, 3758 and 71:
[["define", "new-checkout", false],
["set_rollout", "new-checkout", 30],
["evaluate", "new-checkout", "u0"],
["evaluate", "new-checkout", "u4"],
["evaluate", "new-checkout", "u7"],
["evaluate", "new-checkout", "u8"],
["set_rollout", "new-checkout", 40],
["evaluate", "new-checkout", "u7"]]returns [true, true, false, true, false, true, true, true]. The threshold for 30 is 3000, so u4 (2686) and u8 (71) are in and u0 (5510) and u7 (3758) are out; raising the rollout to 40 (threshold 4000) brings u7 in.
Part 3 — Segment targeting rules (PLAT-561)
Goal: serve a specific value to users who match a segment, using attributes passed at evaluation time.
evaluate now accepts an optional third argument, attrs: a JSON object such as {"plan": "pro", "country": "DE"}. Calls without it behave exactly as before (attrs is {}).
A segment is a named list of conditions; a user is in the segment when all conditions match (logical AND). A rule attaches a segment to a flag with a value to serve.
Conditions are [attribute, operator, operand]:
| Operator | Matches when | Operand |
|---|---|---|
eq | the attribute equals the operand | string, number, boolean or null |
neq | the attribute is present and does not equal the operand | same |
in | the attribute equals one of the operands | a list of scalars |
gte | the attribute is a number >= the operand | a number |
lt | the attribute is a number < the operand | a number |
Rules
define_segmentstores the segment and returnstrue; defining an existing name replaces it, and flags that already use it see the new definition.nameis validated like a flag name (invalid arguments); the condition list must be non-empty and every condition must be a three-element list with a non-empty string attribute, one of the operators above, and an operand of the right shape, otherwiseinvalid condition.- A condition on an attribute the user does not have is false for every operator,
neqincluded. Strings compare case-sensitively. Booleans never equal numbers:trueis not1.gte/ltare false if the attribute is not a number. add_ruleappends a rule to the flag. Check order: unknown flag, unknown segment, theninvalid value(the value must be a boolean). Rules are tried in the order they were added and the first rule whose segment matches wins, even if it servesfalse.- Precedence, highest first: (1) user override, (2) the first matching targeting rule, (3) percentage rollout, (4) flag default. A matching rule that serves
falseends evaluation, so a rollout cannot switch that user back on.
Example
[["define", "search.v2", false],
["define_segment", "pro-eu", [["plan", "eq", "pro"], ["country", "in", ["DE", "FR"]]]],
["add_rule", "search.v2", "pro-eu", true],
["evaluate", "search.v2", "u1", {"plan": "pro", "country": "DE"}],
["evaluate", "search.v2", "u1", {"plan": "pro", "country": "US"}],
["evaluate", "search.v2", "u1"],
["set_override", "search.v2", "u1", false],
["evaluate", "search.v2", "u1", {"plan": "pro", "country": "DE"}]]returns [true, true, true, true, false, false, true, false].
Notes
store.py,models.pyandvalidation.pyare known-good; the Part 1 bugs are in the override handling.- You may use the AI assistant freely. Later parts must not break earlier ones.
- Parts 1-3 are all in scope; there is no stretch part.
Example 1
- Input:
- [["define","new-checkout",false],["evaluate","new-checkout","u1"],["set_override","new-checkout","u1",true],["evaluate","new-checkout","u1"],["evaluate","new-checkout","u2"],["evaluate","dark-mode","u1"]]
- Output:
- [true,false,true,true,false,"error: unknown flag"]
- Explanation
- The flag defaults off. Pinning u1 on turns it on for u1 only; u2 still gets the default and an undefined flag is an error.
Example 2
- Input:
- [["define","new-checkout",false],["set_rollout","new-checkout",30],["evaluate","new-checkout","u0"],["evaluate","new-checkout","u4"],["evaluate","new-checkout","u7"],["evaluate","new-checkout","u8"],["set_rollout","new-checkout",40],["evaluate","new-checkout","u7"]]
- Output:
- [true,true,false,true,false,true,true,true]
- Explanation
- The buckets of u0, u4, u7 and u8 are 5510, 2686, 3758 and 71. At 30% (threshold 3000) only u4 and u8 are in; at 40% u7 (3758) joins.
Example 3
- Input:
- [["define","search.v2",false],["define_segment","pro-eu",[["plan","eq","pro"],["country","in",["DE","FR"]]]],["add_rule","search.v2","pro-eu",true],["evaluate","search.v2","u1",{"plan":"pro","country":"DE"}],["evaluate","search.v2","u1",{"plan":"pro","country":"US"}],["evaluate","search.v2","u1"],["set_override","search.v2","u1",false],["evaluate","search.v2","u1",{"plan":"pro","country":"DE"}]]
- Output:
- [true,true,true,true,false,false,true,false]
- Explanation
- A pro user in Germany matches the segment and gets the rule's value. Other users get the default. An explicit false override beats the matching rule.
Guidelines
- Python 3 standard library only
- Use hashlib for bucketing; Python's built-in hash() is randomised per process
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Flag names, segment names and user ids are case-sensitive strings
- evaluate without attrs must keep working exactly as before