Module 4: Branch by Abstraction

Building the modern implementation behind the abstraction

Overview

In the previous lesson you inserted the ShippingCalculator abstraction and wrapped the old logic in LegacyShipping, without changing behavior. Now comes the second step: building the new implementation behind that same abstraction, alongside the old one. It's the step where you finally write new code —the ModernShipping—, but with a constraint that changes everything: the new implementation meets exactly the same contract as the old one (cost(order) -> float), even though inside it's built differently. And it doesn't touch the legacy: it leaves it intact, running, sustaining the system, while the new one is raised at its side.

This is the new airplane engine mounted alongside the old one, connected to the same adapter. The two engines exist at once. The new one doesn't receive power yet —the flag is still OFF, production uses the legacy—, but it's already there, ready, bolted to the same neutral coupling. The key to this step is coexistence: the two implementations live in the code at the same time, both meet ShippingCalculator, and the abstraction can delegate to either of the two. Neither knows about the other; neither inherits from the other; they share only the contract.

This lesson executes it with a coexistence and parity verification: we set up ModernShipping with a different internal structure from the legacy's —named rates, rules separated into named methods— and we run the two side by side over the known cases, only to compare. The system in production still uses the legacy (the flag is OFF): ModernShipping is ready, but doesn't receive a single call from a real user. That separation —the new one exists but has no traffic— is what makes this step safe.

Connection with the module. Lesson 2 inserted the abstraction (the first step); this one builds the new implementation behind it (the second). Lesson 4 does the third: switch with the flag to give the new one traffic. Lesson 5 does the serious validation with a parallel-run before trusting. Everything that follows assumes ModernShipping already exists and meets the contract: without the new implementation there's nothing to switch. Notice the boundary: building ModernShipping alongside —without touching the legacy— is the internal equivalent of the strangler's "build the new service alongside" (module 3, lesson 3). There the new service was a separate process with the same HTTP contract; here it's a separate class with the same method contract. The principle is identical: the new is built without modifying the old, and they share the contract, not the implementation.

An analogy: the second engine mounted alongside, without removing the first

Let's return to the plane in flight. You already installed the adapter —the universal flange— and the old engine is still bolted to it, pushing. Now you bring the new engine. What you don't do is remove the old one to put the new one: that would leave the plane without an engine for an instant. What you do is mount the new engine on a second coupling of the same adapter, alongside the old one, with the plane flying with the same engine as ever.

During this step, the new engine is mounted but off. It doesn't move the plane: the old one keeps doing all the work. But you can do something very valuable without risking the flight: turn on the new engine on a test bench —or let it idle— and measure that it pushes what it should, that the temperature is correct, that it doesn't vibrate strangely. You compare its readings against the old one's under the same conditions. If the new one gives the same numbers as the old one, you gained confidence without having given it a single passenger.

The new engine mounted and off is ModernShipping built behind the abstraction with the flag OFF: it exists, it meets the same coupling, but it doesn't receive traffic. Turning it on on the ground to compare its readings against the old one is running the two implementations over the same orders and verifying they give the same cost —the parity of this lesson, and the parallel-run of lesson 5—. What matters is that the new engine can have a completely different internal engineering —another turbine design, another injection system— as long as it pushes the same and fits in the same adapter. That's what "same contract, different implementation" means.

Worked example: the coexistence of legacy and modern behind the abstraction

We're going to build ModernShipping alongside LegacyShipping and demonstrate that the two coexist behind the same abstraction, meeting the same contract. The difference is in the internal structure: while the legacy has all its logic crammed into a single method, the modern organizes it —a named rate table (RATES), named constants for the free-shipping threshold and the included weight, and the rules separated into private methods (_qualifies_for_free, _weight_surcharge)—. It's more legible and easier to change, but produces the same result. Then we run the two over the known cases, only to compare, making it clear that production is still on the legacy.

from typing import Protocol

class ShippingCalculator(Protocol):
    def cost(self, order: dict) -> float: ...

# --- LEGACY: intact. The abstraction wraps it; we don't touch it. ---
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)

# --- MODERN: built ALONGSIDE, behind the SAME abstraction. Different internal
# structure (rate table + named rules), but the SAME contract: cost(order). ---
class ModernShipping:
    RATES = {"local": 5.0, "national": 10.0, "international": 25.0}
    FREE_SHIPPING_MIN = 50.0
    WEIGHT_INCLUDED_KG = 1.0
    PER_EXTRA_KG = 2.0

    def _qualifies_for_free(self, order: dict) -> bool:
        return order["order_total"] >= self.FREE_SHIPPING_MIN and order["zone"] != "international"

    def _weight_surcharge(self, order: dict) -> float:
        return max(0.0, order["weight_kg"] - self.WEIGHT_INCLUDED_KG) * self.PER_EXTRA_KG

    def cost(self, order: dict) -> float:
        if self._qualifies_for_free(order):
            return 0.0
        return round(self.RATES[order["zone"]] + self._weight_surcharge(order), 2)

ORDERS = [
    {"id": 1, "zone": "local",         "weight_kg": 0.5, "order_total": 20.0},
    {"id": 2, "zone": "national",      "weight_kg": 1.0, "order_total": 30.0},
    {"id": 3, "zone": "international", "weight_kg": 3.0, "order_total": 80.0},
    {"id": 4, "zone": "local",         "weight_kg": 1.0, "order_total": 60.0},
    {"id": 5, "zone": "national",      "weight_kg": 4.0, "order_total": 55.0},
]

# --- Coexistence: both implement ShippingCalculator. We run the two side by
# side over the known cases, ONLY to compare. The system in production still
# uses legacy (the flag is still OFF): modern is ready, but receives no traffic. ---
legacy, modern = LegacyShipping(), ModernShipping()

print("Coexistence behind the abstraction: both meet the same contract.\n")
print(f"{'order':>6}{'zone':>16}{'legacy.cost':>13}{'modern.cost':>13}{'equal?':>9}")
print("-" * 57)
equal_count = 0
for o in ORDERS:
    lc, mc = legacy.cost(o), modern.cost(o)
    same = lc == mc
    equal_count += same
    print(f"{o['id']:>6}{o['zone']:>16}{lc:>13}{mc:>13}{('YES' if same else 'NO'):>9}")

print("-" * 57)
print(f"\nThey match in {equal_count}/{len(ORDERS)} known cases.")
print("The flag is still OFF: production uses legacy. modern lives alongside, ready.")

What to expect. When you run the file, the output is exactly this:

Coexistence behind the abstraction: both meet the same contract.

 order            zone  legacy.cost  modern.cost   equal?
---------------------------------------------------------
     1           local          5.0          5.0      YES
     2        national         10.0         10.0      YES
     3   international         29.0         29.0      YES
     4           local          0.0          0.0      YES
     5        national          0.0          0.0      YES
---------------------------------------------------------

They match in 5/5 known cases.
The flag is still OFF: production uses legacy. modern lives alongside, ready.

Read the equal? column: YES in the five rows, and the summary confirms it: They match in 5/5 known cases. The two implementations —one crammed, the other organized— produce the same cost in all the cases: 5.0, 10.0, 29.0, 0.0, 0.0. That's behavior parity with a different internal structure. ModernShipping didn't copy the legacy line by line: it re-expressed it with named rates and separated rules, and even so arrives at the same number. Notice especially row 3, the international: ModernShipping replicated the tacit rule (free shipping "and not international") in its _qualifies_for_free method, so it returns 29.0, the same as the legacy. That's the rule lesson 5 will show is easy to lose; here ModernShipping kept it.

And read the last line, which is the most important of this step: The flag is still OFF: production uses legacy. modern lives alongside, ready. ModernShipping exists, meets the contract, and matches the legacy in the known cases —but it doesn't receive a single call from a real user—. Production is still running the legacy. The new implementation is mounted and off, like the plane's second engine. This is the safest possible state of the migration: you have the new one ready and verified, without anyone using it yet. Switching the flag —giving it traffic— is the next step, and it's only done when the serious validation (the parallel-run of lesson 5) is clean.

Deep dive: same contract, different implementation

The constraint that governs this step —"same contract, different implementation"— deserves to be broken down, because it's what makes branch by abstraction work. Here's the structure, with the two implementations hanging from the abstraction:

                    ShippingCalculator  (the contract: cost(order) -> float)
                            │
              ┌─────────────┴─────────────┐
              │                           │
       LegacyShipping              ModernShipping
       (crammed logic,             (named RATES,
        a single method)            rules in methods,
                                    same result)
       [receives traffic TODAY]    [exists, no traffic yet]

What the two share is the contract: they receive an order, they return a float that is the shipping cost. What they don't share is how they arrive at that number. The legacy does it with an inline dictionary and a long condition; the modern with a rate table, named constants, and separated methods. Neither inherits from the other —there's no base class with shared logic—; both, independently, meet the Protocol. This matters for one reason: if ModernShipping inherited from LegacyShipping, it would drag along its logic, and it wouldn't be a new implementation, it would be the old one with patches. Clean coexistence requires that they be sisters, not mother and daughter: two separate implementations of the same contract.

Why re-express the logic instead of copying it? Because the migration's goal isn't to have two identical copies of the legacy —that improves nothing—, but to have a better implementation: more legible, easier to change, without the quirks that are no longer needed. ModernShipping with its named rates and separated methods is easier to maintain than the crammed legacy. But —and this is the delicate balance— it has to produce the same result in the known cases, including the quirks that are still correct behavior (like international not being free). Lesson 5 teaches how to distinguish the quirks to keep (correct behavior) from the bugs that can be fixed —but that's a deliberate decision, not an accident of the reimplementation—. In this step, the goal is parity: same result, better structure.

A detail on why production is still on the legacy even though the modern already matches 5/5: five known cases aren't sufficient proof. Matching on five chosen orders doesn't guarantee matching on the thousands of real orders, with their weird combinations of zone, weight, and total. Giving the modern traffic with only five verified cases would be reckless. That's why the flag stays OFF until the serious validation: the parallel-run that runs the two over many more cases (lesson 5) and the gradual rollout with implicit fallback (lesson 4). This lesson's coexistence is the scaffolding; the confidence to switch is built afterward.

Common mistakes

Making ModernShipping inherit from LegacyShipping. What happens: to "reuse" the logic that already works, the team does class ModernShipping(LegacyShipping) and only overrides some methods. Why it happens: it seems efficient —don't rewrite what's already there—. How to spot it: ModernShipping doesn't compile without LegacyShipping; deleting the legacy (step 5) would break the modern. The two implementations that were supposed to be independent sisters turned out to be mother and daughter. How to fix it: ModernShipping must be an independent implementation of the contract, without inheriting from the legacy. If there's genuinely shared and neutral logic (a utility function without the old one's quirks), extract it to a helper that neither of the two "owns." But the general rule is that the new one is written new, meeting the contract from scratch, so that the day you delete the legacy it drags nothing along. Inheriting from the legacy is tying yourself to it right when you wanted to break free.

Giving the modern traffic with only a few verified cases. What happens: the modern matches the legacy in the example's five cases, and the team concludes "done, it's equivalent" and switches the flag. Why it happens: five YES in a row give a sense of certainty that doesn't correspond to the evidence. How to spot it: if you're about to raise the flag and your only validation is a handful of cases chosen by hand, you don't have enough data. Real orders have combinations your five cases don't cover. How to fix it: this step's coexistence is for building and mounting the new one, not for authorizing it. The authorization comes from the serious validation —the parallel-run over many cases (lesson 5)— and from the gradual rollout that exposes the new one to little traffic first (lesson 4). Five verified cases say "it's worth continuing"; they don't say "switch to 100%."

Taking the chance of the reimplementation to "fix" quirks without deciding it. What happens: when re-expressing the logic, the team sees the rule "international isn't free" and thinks "this looks like a bug, I'll remove it" —and ModernShipping starts to differ from the legacy without anyone having decided it. Why it happens: rewriting invites "improving," and some of the legacy's quirks look like errors. How to spot it: the parallel-run (lesson 5) shows discrepancies that aren't modern bugs, but behavior changes someone introduced "along the way." How to fix it: in this step, the goal is parity, not rule improvement. Every behavior difference relative to the legacy must be an explicit and separate decision, not a side effect of rewriting. If "international isn't free" turns out to be a bug the business wants to correct, it's corrected in a separate, documented change, after the migration is done and stable —not snuck into the reimplementation, where it's confused with a regression—. First migrate to parity; improve the rules later.

Exercises

Exercise 1 — The engine with different engineering. In the airplane analogy, the new engine is mounted alongside the old one, off, and can have a completely different internal engineering. (a) What has to be the same between the new engine and the old one for it to work? (b) What can be different? (c) Why is it valuable to be able to turn it on "on the ground" and compare its readings before giving it a passenger?

See solution

(a) The coupling has to be the same (it fits in the same flange/adapter) and the thrust in the known conditions (it produces the power it should). In the code, that's the contract: the same signature (cost(order) -> float) and the same result in the known cases. It's what makes the engine —or the implementation— interchangeable.

(b) All the internal engineering can be different: the turbine design, the injection system, the materials. In the code, the internal structure: ModernShipping uses named rates, constants, and separated methods where the legacy had everything crammed. The how is free as long as the what (contract and result) is the same.

(c) Because turning it on on the ground and comparing its readings against the old one gives you evidence of parity without risking anyone: if the new engine gives the same numbers as the old one under the same conditions, you gained confidence without having trusted it with a flight with passengers. In the code, running the two implementations over the same orders and comparing (this lesson's coexistence, and lesson 5's parallel-run) validates the new one without exposing it to real users. It's the safest state: the new one verified, without traffic yet.

Exercise 2 — Parity with a different structure. In the example, LegacyShipping and ModernShipping match in 5/5 cases despite being written very differently. (a) What does "they meet the same contract" mean exactly? (b) Why is it desirable for the internal structure to be different, instead of an exact copy of the legacy? (c) Does matching in 5/5 cases authorize switching the flag to 100%? Justify.

See solution

(a) That the two have the same public signature (cost(order) -> float) and produce the same result for the same inputs, without the callers having to know which is which. The contract is the promise observable from outside (what it receives, what it returns); meeting the same contract is being interchangeable from the callers' point of view.

(b) Because the migration's goal isn't to duplicate the legacy —that improves nothing—, but to have a better implementation: ModernShipping, with named rates and rules separated into methods, is more legible and easier to change than the crammed legacy. If you copied the legacy line by line, you'd have two copies of the same problem. The different structure is the point: you migrate to something better keeping the same observable behavior.

(c) No. Five cases chosen by hand don't cover the variety of the real orders —weird combinations of zone, weight, and total that aren't in the sample—. Matching in 5/5 says "it's worth continuing," not "it's equivalent in everything." To authorize the rollout you need the serious validation: a parallel-run over many more cases (lesson 5) and a gradual rollout that exposes the new one to little traffic first, with the old one as a net (lesson 4). This step's coexistence builds the new one; the confidence to switch is earned afterward, with more evidence.

Exercise 3 — Inheritance or independent implementation? A colleague proposes to write ModernShipping like this: class ModernShipping(LegacyShipping), overriding only the method that computes the weight surcharge, "to reuse everything else that already works." (a) What problem does this create for step 5 (delete the legacy)? (b) What principle of clean coexistence is violated? (c) When is it legitimate to share code between the two implementations, and how is it done right?

See solution

(a) It creates a fatal problem for step 5: if ModernShipping inherits from LegacyShipping, you can't delete the legacy without breaking the modern —the modern depends on the old class for everything it didn't override—. The migration could never really end: the legacy would stay "alive underneath" the modern forever, which is exactly what branch by abstraction wants to avoid.

(b) The principle that the implementations must be independent sisters, not mother and daughter is violated: each one meets the contract on its own, without depending on the other. Inheritance ties the modern to the legacy, contaminating it with its logic and its quirks, and makes the clean deletion impossible. ModernShipping must implement ShippingCalculator from scratch, not extend the legacy.

(c) It's legitimate to share code when there's genuinely neutral logic without the old one's quirks —for example, a utility function round_currency(x) or a business constant both use—. It's done right by extracting that logic to a helper that neither of the two "owns": a separate function or module both depend on as equals, not a base class one inherits from the other. The difference is the direction of the dependency: sharing a neutral helper is fine (both depend on something external and stable); inheriting from the legacy is wrong (the modern depends on the old one, which you want to delete). Practical rule: if deleting the legacy breaks the modern, the dependency is badly placed.

Summary and next step

In this lesson you did the second step of branch by abstraction: building the modern implementation behind the abstraction. You saw, with the second engine mounted alongside the old one, that the new implementation is raised without touching the old one, meeting the same contract but with different internal engineering. And you executed it with the coexistence verification: you set up ModernShipping with named rates and separated rules, ran the two implementations side by side over the known cases, and saw They match in 5/5 known cases with the flag still OFF —the new one ready and verified, receiving no real traffic—. You learned the constraint that governs the step: same contract, different implementation; independent sisters, not mother and daughter; behavior parity, with the correct quirks kept and the improvements left for later.

Before moving on you should be able to: explain what "same contract, different implementation" means and why the different structure is desirable; say why ModernShipping must not inherit from LegacyShipping; explain why matching in five cases doesn't authorize the rollout; and distinguish keeping a correct quirk from "fixing along the way" a supposed bug without deciding it.

Lesson 4 does the third step: the feature flag as the switch point. Now that the two implementations coexist behind the abstraction, you're going to set up the flag that decides, on each call, which one runs —and raise it from 0 to 100% gradually, with a stable bucket per order—. You're going to verify the pattern's central property: that switching the implementation doesn't break the callers. And you're going to understand the difference between a flag (runtime, per call, changed hot) and a config (deploy-time, the whole app), which decides how and when the change is made.

Resources

  • Martin Fowler, "BranchByAbstraction" (2014) — martinfowler.com/bliki/BranchByAbstraction.html. Fowler describes the second move: build the new implementation behind the abstraction, coexisting with the old one, before switching. This lesson's step. In English.
  • Sam Newman, Monolith to Microservices (O'Reilly, 2019), ch. 3 — the idea of building the new alongside the old with the same contract, without touching the existing implementation, applied here to a class instead of a service. In English.
  • Robert C. Martin, Clean Architecture (Prentice Hall, 2017) — the principle that the details (concrete implementations like LegacyShipping/ModernShipping) depend on the policies (the ShippingCalculator abstraction), and not the other way around, which is what allows two implementations to coexist without the callers depending on either. In English.
  • Paul Hammant, "branchbyabstraction.com" — branchbyabstraction.com. The step-by-step, including the phase of having the two implementations alive at once behind the abstraction. In English.