Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 54

    Branch By Abstraction

    Branch By Abstraction introduces an abstraction layer (interface) in front of a module you plan to replace, routes all callers through the abstraction, then swaps the implementa…

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Branch By Abstraction introduces an abstraction layer (interface) in front of a module you plan to replace, routes all callers through the abstraction, then swaps the implementation behind it — all on trunk. Netflix uses this to migrate encoding pipelines, recommendation stores, and legacy Cassandra clusters without the merge hell of long-lived feature branches.

    Real production story

    Netflix's transcoding team needed to replace a decade-old FFmpeg wrapper with a GPU-accelerated pipeline. A previous attempt used a 9-month Git branch: 847 commits diverged from main, merge conflicts consumed two engineers full-time for six weeks, and the branch was abandoned.

    The staff engineer introduced Transcoder interface with two implementations: LegacyFfmpegTranscoder and GpuTranscoder. Every caller migrated to the interface in small PRs over three sprints. Behind the abstraction, a feature flag toggled implementation per title tier. After GPU path reached parity on 100% of 4K content, legacy implementation deleted in one 200-line PR. Total trunk disruption: zero merge freezes.

    Business problem

    Business pressure: Netflix ships playback and UI changes daily. A long-lived migration branch blocks hundreds of engineers and delays content launch windows tied to studio contracts.

    • Revenue at risk: Stream start failures during peak hours (Sunday evening US) directly correlate with churn — migration cannot freeze the trunk.
    • Engineering velocity: 2000+ engineers on monorepo trunk; a 6-month branch is organizational poison.
    • Compliance / trust: Studio partners require predictable delivery dates — "we're on a migration branch" is not an acceptable excuse for delayed 4K rollout.

    Architecture overview

    Branch By Abstraction in production: (1) define interface matching current behavior, (2) wrap existing code as LegacyImpl, (3) migrate all callers to interface, (4) build NewImpl behind same interface, (5) flip flag, (6) delete legacy. Git branch is never the migration vehicle — the abstraction is.

    • Definition: Trunk-based replacement pattern using an abstraction seam to swap implementations without diverging version control history.
    • When to adopt: Module with many callers, trunk-based workflow, and need to run old + new implementations in parallel.
    • When to defer: Single caller, small module, or team can replace in one PR without abstraction ceremony.
    • Operability: Feature flag per implementation; metrics comparing legacy vs new on same interface methods.

    Architecture motivation

    Why architects care: Branch By Abstraction keeps migration on trunk while isolating the replacement behind a stable contract. The naive alternative — long-lived Git branch — creates merge debt that often kills the migration entirely.

    • Force: Core module must be replaced but has 40+ callers across services.
    • Constraint: Trunk-based development is non-negotiable; no team gets a private branch for quarters.
    • Outcome: Interface stable; implementations swappable via DI and feature flags; legacy deleted when parity proven.

    Internal architecture

    Netflix transcoding abstraction — all pipelines depend on interface, not implementation:

    text
    ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
    │ Ingest Svc │ │ Catalog Svc │ │ Preview Gen │
    └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
    │ │ │
    └────────────────┼─────────────────┘
    ┌─────────────────────┐
    │ Transcoder (iface) │
    └──────────┬──────────┘
    ┌────────────┴────────────┐
    ↓ ↓
    LegacyFfmpegTranscoder GpuTranscoder
    (feature flag: off) (feature flag: on)
    ↓ ↓
    CPU cluster GPU fleet (A100)

    Data flow

    Primary path: ingest service calls transcoder.encode(titleId, profile) — DI container resolves implementation based on Archaius feature flag and title metadata (4K → GPU, SD → legacy until parity).

    • Write path: encoded artifact written to S3 with content-hash naming — implementation-agnostic.
    • Read path: playback service reads artifact URI from catalog — never knows which transcoder produced it.
    • Async path: encoding completion event emitted by both implementations with identical schema.
    typescript
    interface Transcoder {
    encode(job: EncodeJob): Promise<EncodedArtifact>;
    supportedProfiles(): Profile[];
    }
    class TranscoderFactory {
    constructor(
    private legacy: LegacyFfmpegTranscoder,
    private gpu: GpuTranscoder,
    private flags: FeatureFlags,
    ) {}
    resolve(job: EncodeJob): Transcoder {
    if (job.profile.includes("4K") && this.flags.isEnabled("gpu-transcoder", job.titleId)) {
    return this.gpu;
    }
    return this.legacy;
    }
    }
    // Callers inject factory — never concrete impl
    class IngestService {
    constructor(private transcoderFactory: TranscoderFactory) {}
    async process(job: EncodeJob) {
    const transcoder = this.transcoderFactory.resolve(job);
    return transcoder.encode(job);
    }
    }

    System design diagram

    Two diagrams show the Branch By Abstraction topology and the primary request/event path used in production at scale.

    Branch By Abstraction — system view
    Callers
    Edge
    Abstraction
    Core
    Legacy impl
    Data
    New impl
    Async
    High-level topology for Branch By Abstraction.
    Branch By Abstraction — request / event flow
    Define interface
    Ingress
    Migrate callers
    Store
    Build new impl
    Store
    Flip flag
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Netflix-style DI wiring with Archaius-compatible feature flag and contract test hook:

    typescript
    // Contract test at abstraction boundary — runs in CI for both impls
    describe("Transcoder contract", () => {
    for (const Impl of [LegacyFfmpegTranscoder, GpuTranscoder]) {
    describe(Impl.name, () => {
    it("produces playable artifact for golden job set", async () => {
    const transcoder = new Impl(testConfig);
    for (const job of GOLDEN_JOBS) {
    const artifact = await transcoder.encode(job);
    expect(await playbackValidator.isPlayable(artifact)).toBe(true);
    }
    });
    });
    }
    });

    Enterprise case study

    Netflix GPU transcoding migration: Branch By Abstraction on trunk with Archaius flags, contract tests at Transcoder interface, and automated parity on 500-title golden set nightly.

    • Before: 9-month abandoned branch; 847 divergent commits; zero GPU titles in prod.
    • Decision: Transcoder interface + factory + tier-based flags; ESLint blocks legacy direct imports.
    • After: 100% 4K on GPU in 14 weeks on trunk; legacy impl deleted; no merge freeze.

    Trade-offs

    • Abstraction tax vs merge hell: Interface + factory adds code — cheaper than 6-month branch merge.
    • Behavioral parity: Interface hides impl differences until playback breaks — invest in contract tests at abstraction boundary.
    • Flag sprawl: Per-title flags multiply — consolidate to tier-based rules with audit trail.

    Security considerations

    Security is architectural: Both implementations handle studio DRM keys — abstraction must not leak key material differently between impls.

    • Identity: Both impls use same IAM role for S3 write — no separate credentials per implementation.
    • Data: Encrypted artifacts use identical KMS keys regardless of transcoder path.
    • Supply chain: GPU container images scanned independently; both impls must pass same CVE gate before flag enable.

    Scalability analysis

    Scale dimensions: Netflix encodes 1000+ titles daily across global regions — abstraction must not add synchronous resolution overhead or single-point bottlenecks.

    • Horizontal scale: Both implementations scale independently; factory is stateless.
    • Hot spots: Blockbuster launch day spikes single-title encode queue — GPU pool autoscales separately from legacy CPU pool.
    • Cost: Running dual implementations during parity period doubles encode compute — time-box to 8–12 weeks with explicit decommission date.

    Failure scenarios

    What breaks: GPU impl produces artifacts that play on 80% of devices; interface contract too narrow and misses HDR metadata; callers bypass abstraction via import of legacy class.

    • Contract too narrow: New impl needs extra parameter not on interface — resist breaking abstraction; extend interface with default method.
    • Bypass imports: Lint rule blocks direct import of LegacyFfmpegTranscoder outside factory module.
    • Flag stuck on: GPU enabled for titles failing parity — automatic rollback when error rate exceeds SLO burn.

    Staff engineer insights

    • The abstraction interface should match what callers need today — not the ideal API you wish you had. Refactor interface after legacy deletion.
    • Migrate callers to the abstraction before building the new implementation — otherwise you optimize the wrong contract.
    • Set a calendar date to delete legacy impl. Abstractions without decommission deadlines become permanent indirection layers.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionHow is Branch By Abstraction different from Strangler Fig at Netflix?+

    Answer

    Branch By Abstraction swaps implementation behind a stable interface on trunk — same deployment unit, same routing. Strangler Fig routes traffic between separate systems (legacy monolith vs new service) at a facade. Use abstraction for in-process/module replacement; strangler for system-level replacement.

    Follow-up

    When would you combine both patterns?
    2AdvancedQuestionCallers keep importing the legacy class despite the abstraction. How do you enforce the seam?+

    Answer

    ESLint/dependency-cruiser rule: ban imports of Legacy* outside factory module. CI fails on violation. Code mod (jscodeshift) to migrate remaining callers. Architecture review checklist item: "no direct legacy imports in diff."

    Follow-up

    What if legacy class is in the same package as the interface?
    3AdvancedQuestionThe new implementation passes unit tests but fails on 15% of production title profiles. Roll back or push forward?+

    Answer

    Roll back flag for failing profiles immediately — trunk stays green. Expand golden set to include failing profiles. Never "fix forward" under full traffic without root cause. Parity period exists precisely for this discovery.

    Follow-up

    How do you structure the golden set to represent long-tail edge cases?

    Architecture review questions

    • Are quality attributes (latency, availability, consistency) explicit with SLOs for Branch By Abstraction?
    • Is the failure/degraded mode documented — including what happens when dependencies are down?
    • Are boundaries and ownership clear on an architecture diagram a new engineer understands in 10 minutes?
    • Is there an ADR capturing alternatives considered and why they were rejected?
    • Can this design scale 10× on traffic and 3× on engineering headcount without a rewrite?
    • Security: authn/authz, encryption, and blast radius reviewed at every external interface?

    Summary

    Branch By Abstraction at Netflix scale means introducing a stable interface, migrating callers on trunk, and swapping implementations behind feature flags — never behind a 6-month Git branch. Master the seam, enforce it with tooling, and time-box legacy deletion.

    Ready to mark this lesson complete?Track your journey across the entire course.