Module 4: Branch by Abstraction
The feature flag as the switch point
Overview
You already have the abstraction inserted (lesson 2) and the two implementations coexisting behind it (lesson 3). LegacyShipping receives all the traffic; ModernShipping is mounted and off. What's missing is the piece that turns the new one on little by little: the feature flag. The flag is the knob that decides, on each call to the shipping calculation, which implementation runs behind the abstraction. It's the valve that directs the power between the plane's two engines: first a trickle to the new one, then more, up to 100%.
This is the mechanical heart of the pattern, and it has a property you have to see executed to believe: switching the implementation doesn't touch the callers. Not one line of checkout_total, the cart, or the admin panel changes when you raise the flag from 0 to 100%. They keep asking the abstraction for .cost(); the flag, underneath, decides who responds. That separation —the caller bolted to the adapter, not to the engine— is what makes the change safe and gradual: you can give the new implementation 10% of the traffic, observe, and go up, without any caller finding out or breaking.
This lesson executes it with a gradual rollout: an in-memory feature_flag with a rollout_percent that rises from 0 to 100, and a stable bucket per order —the same order always falls on the same side— that splits the traffic reproducibly. We're going to measure how many calls go to each implementation at each level, and verify, order by order, that the result the caller sees is the same with the switched implementation as with the legacy always. And we're going to distinguish a concept that's confused: the flag (runtime, per call, changed hot) versus the config (deploy-time, the whole app), because they decide how and when the change is made.
Connection with the module. Lesson 3 built ModernShipping; this one sets up the flag that gives it traffic. Lesson 5 does the serious validation (parallel-run) that must be clean before raising the flag —the real order is validate first, switch after—. Lesson 6 deletes the legacy when the flag reached 100% and stabilized. Notice the boundary: the flag here decides per function call, inside the process —it's internal—. The strangler's by-percentage diversion (module 3, lesson 4) decided per HTTP request, in an external proxy. The stable-bucket mechanics is the same in both; what changes is the decision point: here, in the code, which object is instantiated behind the abstraction; there, on the network, which service the request is routed to.
An analogy: the valve that directs the power between the two engines
In the plane, you already have the old engine pushing and the new engine mounted alongside, off. The valve that directs the power is the feature flag. It's not an all-or-nothing switch: it's a graduated valve. You can put it at "0% to the new" (all the thrust still comes from the old engine), at "10% to the new" (a bit of power through the new one, most through the old), at "50%", at "100%" (all the thrust through the new one, the old one idling).
What matters about this valve is where it's not connected: it's not connected to the pilot's controls. The pilot keeps moving the same throttle, asking for the same power; the valve, underneath, decides which engine that power comes from. The pilot doesn't have to learn a new control or change how they fly: for them, the plane responds the same. That's the key property —the caller (the pilot) doesn't change when you switch the implementation (the engine)—, and it's exactly what we're going to verify in the code.
And there's a second distinction the valve illuminates: the difference between turning the valve in flight and redesigning the plane in the hangar. Turning the valve is a hot adjustment: you do it while the plane flies, gradual, reversible —if the new engine coughs, you turn the valve back to the old one on the spot—. That's the feature flag: it's changed at runtime, per call, without "landing" the system. Redesigning in the hangar would be changing the power wiring in a fixed way, for all flights, and would require bringing the plane down: that's the deploy config —it's decided before startup, applies to the whole app, and changing it requires a redeploy—. Both have their place; to migrate little by little and be able to revert on the spot, you want the valve in flight, not the redesign in the hangar.
Worked example: the flag's rollout without touching the callers
We're going to set up the flag and raise it from 0 to 100% over a fixed batch of 200 orders, measuring the split and verifying that the callers don't break. The feature_flag lives in memory (rollout_percent) and decides per order using a stable bucket: it turns the order's id into a number from 0 to 99 with a hash (crc32), and if that number falls below the rollout_percent, the order uses ModernShipping; if not, LegacyShipping. That the bucket be stable matters: the same order always falls on the same side, so the decision is reproducible and the same order doesn't jump implementation between two calls. The caller, checkout_total, is identical regardless of the rollout: it receives the abstraction and asks it for .cost().
import zlib
from typing import Protocol
class ShippingCalculator(Protocol):
def cost(self, order: dict) -> float: ...
class LegacyShipping:
def cost(self, order: dict) -> float:
base = {"local": 5.0, "national": 10.0, "international": 25.0}[order["zone"]]
surcharge = max(0.0, order["weight_kg"] - 1.0) * 2.0
cost = base + surcharge
if order["order_total"] >= 50.0 and order["zone"] != "international":
cost = 0.0
return round(cost, 2)
class ModernShipping:
RATES = {"local": 5.0, "national": 10.0, "international": 25.0}
def cost(self, order: dict) -> float:
base = self.RATES[order["zone"]]
over = max(0.0, order["weight_kg"] - 1.0)
free = order["order_total"] >= 50.0 and order["zone"] != "international"
return 0.0 if free else round(base + over * 2.0, 2)
# --- The flag lives IN MEMORY and is changed hot, without a redeploy. It decides, order
# by order, which implementation runs behind the abstraction. The stable bucket
# makes the same order always fall on the same side (reproducible, no jumps). ---
def bucket(order_id: int) -> int:
return zlib.crc32(str(order_id).encode()) % 100
class FeatureFlag:
def __init__(self, rollout_percent: int = 0):
self.rollout_percent = rollout_percent
def use_modern(self, order: dict) -> bool:
return bucket(order["id"]) < self.rollout_percent
def resolve_calculator(order: dict, flag: FeatureFlag) -> ShippingCalculator:
return ModernShipping() if flag.use_modern(order) else LegacyShipping()
# Any caller. It's IDENTICAL regardless of the rollout: it receives the abstraction.
def checkout_total(order: dict, calc: ShippingCalculator) -> float:
return round(order["items_subtotal"] + calc.cost(order), 2)
ZONES = ["local", "national", "international"]
ORDERS = [{
"id": i, "zone": ZONES[i % 3],
"weight_kg": round(0.5 + (i % 5) * 0.5, 1),
"order_total": 20.0 + (i % 7) * 10.0,
"items_subtotal": 20.0 + (i % 7) * 10.0,
} for i in range(1, 201)]
legacy_always = LegacyShipping()
print("Switching the flag does NOT touch the callers. Rollout over 200 orders:\n")
print(f"{'rollout':>9}{'-> modern':>12}{'-> legacy':>12}{'callers OK?':>14}")
print("-" * 47)
for pct in (0, 25, 50, 75, 100):
flag = FeatureFlag(rollout_percent=pct)
counts = {"modern": 0, "legacy": 0}
callers_ok = True
for o in ORDERS:
calc = resolve_calculator(o, flag)
counts["modern" if isinstance(calc, ModernShipping) else "legacy"] += 1
# The result the caller sees is the same as with legacy always:
# switching the implementation doesn't change what the checkout returns.
callers_ok = callers_ok and (checkout_total(o, calc) == checkout_total(o, legacy_always))
print(f"{pct:>8}%{counts['modern']:>12}{counts['legacy']:>12}{str(callers_ok):>14}")
print("\n The split moves with a single knob (rollout_percent, in memory).")
print(" 'callers OK? = True' at every level: no caller breaks when switching.")
What to expect. When you run the file, the output is exactly this:
Switching the flag does NOT touch the callers. Rollout over 200 orders:
rollout -> modern -> legacy callers OK?
-----------------------------------------------
0% 0 200 True
25% 52 148 True
50% 104 96 True
75% 148 52 True
100% 200 0 True
Read the table top to bottom: it's the whole rollout in five rows. At 0%, none of the 200 orders uses ModernShipping —all go to the legacy—, exactly as at the end of lesson 3: the new one exists but receives no traffic. At 25%, 52 orders (of 200) fall below the threshold and use the modern; the other 148 stay on the legacy. That's your initial trickle: a fraction of the traffic testing the new implementation while the majority stays on the old one. At 50% the split is almost half and half (104 and 96), and at 100% the 200 orders use the modern and the legacy receives not a single call —the old engine idling, ready to be disassembled (lesson 6)—.
But the column that matters is the last: callers OK?, and it gives True at the five levels. This is the pattern's central property, measured. For each of the 200 orders, at every rollout level, the result of checkout_total with the implementation the flag chose is identical to the result with the legacy always. That is: switching the implementation —giving the order to the modern instead of the legacy— didn't change what the caller returns. The checkout_total wasn't touched even once; the flag moved the traffic underneath it, and the caller didn't even find out. That's the pilot who moves the same throttle while the valve, underneath, changes engine.
That at 25% it comes out 52 and not exactly 50 isn't an error: the hash bucket splits evenly but not to the millimeter over 200 orders. And since the bucket is deterministic (crc32(str(id)) % 100), running the same code again with the same 200 ids gives exactly the same split: there's no chance, there's a fixed hash. What matters isn't the exact number, but that the traffic moves from the old to the new in a controlled way when you raise a single knob —the rollout_percent— without touching anyone else.
Deep dive: flag vs config, and why the flag goes in memory per call
The distinction between feature flag and config seems subtle but decides how the change is made. Here they are, side by side:
FEATURE FLAG CONFIG (deploy)
When it decides at runtime, per call before startup, fixed
Scope per order / per user the whole app at once
Change it hot, without a redeploy requires a redeploy
Reversion instant (lower the flag) another redeploy
Gradualness yes (0 -> 10 -> 50 -> 100) no (on/off for everyone)
Analogy turn the valve in flight rewire in the hangar
For branch by abstraction you want a flag, not a config, and for a concrete reason: the gradualness and the hot reversion. With a flag, you give 10% of the traffic to the modern, observe, and if something goes wrong you lower the flag to 0 on the spot —without a redeploy, without "landing" the system—. With a deploy config, your only option would be to turn the modern on for everyone at once with a deployment, and if it fails, turn it off with another deployment: it's on/off for the whole app, slow to revert, and without a canary. The flag is what turns the switch into gradual and reversible; the config would make it a big-bang.
That's why, in the example, the FeatureFlag lives in memory and decides per order: flag.use_modern(order) is evaluated on each call, looking at the current rollout_percent. In a real system, that rollout_percent would come from a flags service (LaunchDarkly, Unleash, a table in the database, a variable that can be changed without a redeploy) to be able to raise and lower it hot. The exact form of the flag's backend doesn't matter for the pattern; what matters is the property: it's changed at runtime, it's gradual, and it's reverted instantly. Here we simulate it with an integer in memory, which is the essence without the infrastructure.
A nuance on the stable bucket and why it's computed on the order's id (or, in many real cases, on the user's id). If the flag decided at random on each call —with random() without a bucket—, the same order could use the modern on one call and the legacy on the next, giving results that bounce between the two implementations within a single operation. The stable bucket avoids it: bucket(id) is deterministic, so an order always falls on the same side as long as the rollout_percent doesn't change. When the bucket is computed on the user, the property becomes even more valuable: a user sees consistently the modern or the legacy during their session, without jumps that would show them one shipping cost in the cart and another in the checkout. The bucket's stability is what makes the gradual rollout safe for the user, not just measurable.
Common mistakes
Confusing the flag with a deploy config. What happens: the team "switches" with an environment variable read at startup (USE_MODERN_SHIPPING=true), and to change it you have to redeploy. Why it happens: it's the easiest to set up —an environment variable already exists—. How to spot it: you can't give the modern 10% of the traffic; you can only turn it on for everyone or for no one, and changing it requires a deployment. How to fix it: for branch by abstraction you need a runtime flag, per call, changeable hot, not a deploy config. The difference isn't cosmetic: the flag gives you the canary (10% first) and the instant reversion (lower the flag if it fails), which are exactly what makes the switch safe. A deploy config turns the change into a big-bang on/off, without gradualness or fast fallback —exactly what the pattern wants to avoid—.
An unstable bucket that makes the user jump between implementations. What happens: the flag decides with random() < rollout_percent/100 on each call, without a stable bucket. Why it happens: it seems equivalent —"it splits the same percentage at random"—. How to spot it: the same order or user gets the modern on one call and the legacy on the next. In the shipping calculation, that can show one cost in the cart and a different one in the checkout for the same order, within the same session. How to fix it: compute the bucket deterministically over a stable key (the order's id or, better, the user's): bucket(id) < rollout_percent. That way the same order always falls on the same side as long as the percentage doesn't change, and the user sees a consistent implementation. Chance splits the correct percentage on average, but breaks the per-entity consistency; the stable bucket gives both.
Putting business logic in the flag's resolution point. What happens: the resolve_calculator fills up with rules —"if it's international use the legacy, if the total is high use the modern, except on Tuesdays"— until the switch point has more logic than the implementations. Why it happens: the resolution point touches all the calls, it seems convenient to put special conditions in. How to spot it: resolve_calculator stops being "choose by the flag" and starts being "choose by business rules." Now you have a third piece of logic —besides the legacy and the modern— that also has to be maintained and that confuses which result is whose. How to fix it: the resolution point must do one thing: consult the flag and return the corresponding implementation. Every special behavior case lives inside an implementation, not in the selector. If international needs different treatment, that's ModernShipping's (or the legacy's) logic, not resolve_calculator's. The selector stays thin: ask the flag, hand over the object, nothing more.
Exercises
Exercise 1 — The valve and the pilot. In the airplane analogy, the valve that directs the power (the flag) isn't connected to the pilot's controls (the callers). (a) Why is it essential that the pilot doesn't have to change how they fly when you turn the valve? (b) What difference is there between "turning the valve in flight" and "rewiring the power in the hangar"? (c) Which of the two corresponds to a feature flag and which to a deploy config, and why for migrating you want the first?
See solution
(a) Because if the pilot had to change how they fly every time you turn the valve, switching engines would stop being transparent: each change of split would require retraining the pilot (rewriting the callers). The value of the pattern is that the switch point is underneath the pilot: they ask for the same power (call the same abstraction) and the valve, on its own, decides which engine it comes from. In the code, checkout_total isn't touched when you raise the flag —that's the callers OK? = True property—.
(b) "Turning the valve in flight" is a hot adjustment, gradual and reversible on the spot: you do it without bringing the plane down, you give a bit of power to the new engine and, if it coughs, you go back to the old one on the spot. "Rewiring the power in the hangar" is a fixed change for all flights, that requires bringing the plane down (redeploy) and is on/off, without gradualness.
(c) Turning the valve in flight is the feature flag (runtime, per call, hot, gradual, reversible); rewiring in the hangar is the deploy config (fixed, the whole app, requires a redeploy, on/off). For migrating you want the flag because it gives you the canary (give the modern little traffic first) and the instant reversion (lower it if it fails), which are what makes the switch safe. The config would force you into a big-bang: turn the modern on for everyone at once and turn it off with another deployment if it goes wrong.
Exercise 2 — Read the rollout. In the example, at rollout_percent=25 the split was 52 to the modern and 148 to the legacy over 200 orders, with callers OK? = True. (a) Why didn't it come out exactly 50 and 150? (b) If you run the same code again with the same 200 ids, will the split be the same or different? (c) What does it mean, in terms of the pattern, that callers OK? gives True at the 25% level?
See solution
(a) Because the bucket is computed with a hash (crc32) of the id, and a hash splits the ids evenly but not to the millimeter. Of 200 ids, approximately 25% fall below 25, but "approximately" isn't "exactly": 52 came out. Over larger samples the proportion gets closer to 25%; what matters is that the fraction is controllable with the rollout_percent, not that it's exact.
(b) It'll be exactly the same. The bucket is deterministic (crc32(str(id)) % 100): the same id always produces the same bucket, so the same batch of ids always produces the same split. There's no chance; there's a fixed hash. That's why the example is reproducible: you can run it a thousand times and at 25% those same 52 orders will always come out.
(c) It means that, for the 52 orders that at 25% use ModernShipping and the 148 that use LegacyShipping, the result the caller sees (checkout_total) is identical to what it would see with the legacy always. That is: giving those 52 orders to the modern changed nothing about what the checkout returns —the new implementation produces the same costs as the old one in those cases, and the caller, which depends on the abstraction, didn't notice the switch—. It's the pattern's central property (switching doesn't break the callers) verified at that rollout level. (Note: here it gives True because ModernShipping matches the legacy in all these cases; in lesson 5 we'll see what happens when it doesn't match, and why the validation goes before raising the flag.)
Exercise 3 — Design the rollout gate. The example raises the flag straight to each level (0, 25, 50, 75, 100) to illustrate the split. In reality you wouldn't go up blind. (a) What condition should you verify before raising the rollout_percent from one level to the next? (b) What would you do if, with the flag at 25%, you started to see errors in the new implementation? (c) Why is the correct order "validate first, raise the flag after" and not the other way around?
See solution
(a) Before going up, you should verify that the new implementation is healthy in the traffic it already receives: that it doesn't throw errors, that its latency is acceptable and —the central thing for the shipping calculation— that it matches the legacy in the cases being cross-checked (the parallel-run of lesson 5 at 0 discrepancies). A rollout gate only promotes to the next level if those conditions are met; if not, it stays or goes down.
(b) You'd lower the flag on the spot —to 0%, or to the previous safe level— without a redeploy, because the flag is changed hot. That returns all the traffic to the legacy and stops the damage immediately (only 25% of the users saw the problem, not 100%, and for a short time). Then you investigate and fix ModernShipping calmly, with the flag at 0, without production pressure. The instant reversion is exactly what a flag gives you and a deploy config doesn't.
(c) Because raising the flag is exposing the new implementation to real users. If you go up first and validate after, every user in the switched percentage is a test in production without a net: if the modern differs from the legacy in their case, it already overcharged or undercharged before you detected it. Validating first —with the parallel-run, without exposing anyone— catches the discrepancies before they reach a user. The flag is raised only over what's already validated. It's the same logic as the plane: first you turn on the new engine on the ground and check it pushes well; only after that do you trust it with power in flight.
Summary and next step
In this lesson you did the third step of branch by abstraction: setting up the feature flag as the switch point. You saw, with the valve that directs the power between the two engines without touching the pilot's controls, that the flag decides which implementation runs without the callers changing. And you executed it with a gradual rollout: you raised the rollout_percent from 0 to 100% over 200 orders with a stable bucket, measured the split move from the old to the new, and verified callers OK? = True at the five levels —the pattern's central property, measured—. You learned the distinction that decides how the change is made: the flag (runtime, per call, hot, gradual, reversible) versus the deploy config (fixed, the whole app, big-bang), and why the stable bucket keeps each order and user on a consistent implementation.
Before moving on you should be able to: explain why switching the flag doesn't touch the callers; distinguish a flag from a config and say why the pattern needs a flag; explain what the stable bucket guarantees and why a random bucket breaks the per-user consistency; and describe the correct order —validate first, raise the flag after— and what you'd do if the modern failed mid-rollout.
Lesson 5 does exactly that validation: the parallel-run to prove the change is safe. Now that you know how to switch the flag, you're going to learn what has to be verified before raising it. You're going to set up a parallel-run that calls the two implementations, compares their results, and reports discrepancies —always returning the legacy's value, without exposing the new one—. And you're going to catch a real bug: a ModernShipping that forgot a tacit rule of the legacy, which the parallel-run gives away in a known case. Only when the run gives 0 discrepancies will it be safe to raise the flag you just set up.
Resources
- Martin Fowler and Pete Hodgson, "Feature Toggles (aka Feature Flags)" (2017) — martinfowler.com/articles/feature-toggles.html. The reference on the types of flags (release, ops, experiment) and why a runtime flag is different from a deploy config. The direct framework of this lesson. In English.
- Martin Fowler, "BranchByAbstraction" (2014) — martinfowler.com/bliki/BranchByAbstraction.html. Describes how the flag (or toggle) switches between the two implementations behind the abstraction, and how it's raised gradually. In English.
- Pete Hodgson, "Feature toggles are one of the worst kinds of technical debt" — on why the flags must be temporary (they're removed when the migration ends, lesson 6) and not accumulate in the code. In English.
- Jez Humble and David Farley, Continuous Delivery (Addison-Wesley, 2010) — the role of feature toggles to decouple the deployment of the code from the activation of the functionality, which is what allows raising the flag gradually and independently of the deploy. In English.