Branch by Abstraction — Tasks¶
Source: Paul Hammant, "Branch by Abstraction"; Jez Humble & David Farley, Continuous Delivery
Hands-on and scenario exercises. Try each before reading the worked solution. Code is Java; the reasoning is language-agnostic.
Task 1 — Extract the seam (Phase 1)¶
You have a client coupled directly to a concrete class:
public class HttpClient { // hand-rolled, concrete
public String get(String url) { /* uses java.net */ return "..."; }
public String post(String url, String body) { /* ... */ return "..."; }
public void close() { /* ... */ }
}
public class WeatherService {
private final HttpClient http = new HttpClient(); // <-- coupled + self-instantiated
public Forecast today() { return parse(http.get("/today")); }
private Forecast parse(String s) { return new Forecast(); }
}
Do: perform Phase 1 only — introduce the abstraction and make the existing class conform — without changing behavior. State the behavior-preserving steps.
Worked solution
// 1. Extract an interface from what the client actually uses.
public interface HttpGateway {
String get(String url);
String post(String url, String body);
}
// 2. Old class implements it. (Note: close() was NOT in the client's usage,
// so it's excluded — define the seam by client need, not class surface.)
public class HttpClient implements HttpGateway { /* body unchanged */ }
Task 2 — Invert the dependency (Phase 2)¶
Continue from Task 1. WeatherService still does new HttpClient() internally. Route it through the abstraction.
Worked solution
public class WeatherService {
private final HttpGateway http; // abstraction
public WeatherService(HttpGateway http) { this.http = http; } // injected
public Forecast today() { return parse(http.get("/today")); }
private Forecast parse(String s) { return new Forecast(); }
}
// Composition root now owns the wiring:
HttpGateway gateway = new HttpClient();
WeatherService service = new WeatherService(gateway);
Task 3 — Ship the new impl dark (Phase 3)¶
You now want a new HTTP gateway built on a modern client library. Add it behind the same seam, selectable by a flag, defaulting off.
Worked solution
public class ModernHttpGateway implements HttpGateway {
private final ModernClient client;
public ModernHttpGateway(ModernClient client) { this.client = client; }
@Override public String get(String url) { return client.request("GET", url, null); }
@Override public String post(String url, String body) { return client.request("POST", url, body); }
}
// Composition root chooses, default OLD:
HttpGateway gateway = flags.isEnabled("use-modern-http")
? new ModernHttpGateway(modernClient)
: new HttpClient();
Task 4 — Design the flip & rollback (Phase 4)¶
Write the rollout plan and the rollback trigger conditions for flipping use-modern-http on a customer-facing path.
Worked solution
**Rollout:** internal traffic → 1% → 5% → 25% → 100%, pausing at each step to watch dashboards. **Observability (tagged by `impl`):** error rate, latency p50/p95/p99, and any new-downstream saturation. **Pre-agreed abort conditions (decide before flipping):** - new-impl error rate exceeds old by > X%, or - p99 latency regression > Y%, or - any correctness anomaly. **Rollback:** flip `use-modern-http` to OFF → 100% routes back to the proven `HttpClient` in seconds; confirm dashboards return to baseline; preserve logs/traces; diagnose off the hot path; re-ramp from a low percentage. Lesson: the plan is written *before* the ramp. The flag's value is the ramp and the instant rollback — using neither is just a riskier deploy.Task 5 — Clean up (Phase 5), keep-or-inline decision¶
The flip baked at 100% for a month. Perform Phase 5 and justify whether to keep or inline HttpGateway.
Worked solution
Steps: 1. Delete `HttpClient` and its tests (compiler confirms no references). 2. Delete the `use-modern-http` flag and the ternary. 3. **Keep `HttpGateway`.** It's a genuine boundary: it isolates external I/O, aids testing (you can inject a fake gateway), and makes future swaps easy. This is *architecture*, not scaffolding. Contrast: if the seam had been something like `OrderTotalCalculator` with only ever one real implementation and no testing benefit, you'd inline it instead. The test is "does this boundary earn its keep after the migration?"Task 6 — Argue against using the technique¶
A teammate proposes a full Branch by Abstraction (interface + runtime flag + 5 phases) to rename a method calc() to calculateTotal() on a class with eight call sites, all in your own module. Should they? Make the case.
Worked solution
No. This change lands atomically: an IDE **Rename** refactoring updates all eight call sites in one safe, behavior-preserving commit that the compiler verifies. There's no big-bang risk, no coexistence window, nothing to roll back gradually. A full Branch by Abstraction here would be pure overhead — an interface and a flag that exist for no reason, scaffolding to erect and then tear down for a thirty-second job. *If* the method were a **published** interface consumed outside your control (other teams/services depend on the name), then you wouldn't do Branch by Abstraction either — you'd do **Parallel Change**: add `calculateTotal()`, deprecate `calc()`, let consumers migrate, then remove `calc()`. See [Parallel Change](../02-parallel-change-expand-contract/junior.md). Branch by Abstraction is for swapping *components*, not renaming methods.Task 7 — Shadow-compare a read (verifying variant)¶
You're swapping a pricing engine and want production confidence before trusting the new one. Implement a comparing wrapper for the read-only price(Cart) method.
Worked solution
public class ComparingPricingEngine implements PricingEngine {
private final PricingEngine authoritative; // old, trusted
private final PricingEngine candidate; // new
private final Metrics metrics;
@Override public Money price(Cart cart) {
Money real = authoritative.price(cart);
try {
Money cand = candidate.price(cart);
if (!real.equals(cand)) {
metrics.increment("pricing.mismatch");
// optionally log cart id + both values for diagnosis
}
} catch (RuntimeException e) {
metrics.increment("pricing.candidate_error");
}
return real; // user always gets the proven price
}
}
Task 8 — Sequence two migrations¶
Your platform needs to swap both the persistence layer and the message broker. Persistence is on a hot path used everywhere; the broker is used by three background workers. Both are independent boundaries. How do you sequence them?
Worked solution
1. **Run them as two independent Branch-by-Abstraction efforts** — they're on different seams (`OrderRepository`, `MessagePublisher`) and don't collide, so they *can* proceed in parallel. 2. **But cap WIP and stagger the risky flips.** Don't have both at an active traffic ramp on the same week — overlapping cutovers make incident attribution hard. 3. **Start the broker migration's flip first.** Smaller blast radius (three workers), high observability, idempotent-ish — good place to validate your process. 4. **For persistence, migrate reads before writes**, shadow-compare reads, and keep the coexistence window short because it touches a hot path. 5. **Use the [Mikado Method](../03-mikado-method/junior.md)** if you discover hidden dependencies between them (e.g., the workers read from the store the persistence migration is changing) — Mikado surfaces the order; Branch by Abstraction executes each node. Lesson: parallelize independent seams, serialize the *flips*, finish before starting more.Next¶
- find-bug.md — diagnose misapplied Branch by Abstraction.
- optimize.md — turn rough situations into phased plans.
- Back to junior.md · middle.md · interview.md
In this topic