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…
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:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐│ 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.
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 implclass 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.
Production code example
Netflix-style DI wiring with Archaius-compatible feature flag and contract test hook:
// Contract test at abstraction boundary — runs in CI for both implsdescribe("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.
1AdvancedQuestionHow is Branch By Abstraction different from Strangler Fig at Netflix?+
Answer
Follow-up
2AdvancedQuestionCallers keep importing the legacy class despite the abstraction. How do you enforce the seam?+
Answer
Follow-up
3AdvancedQuestionThe new implementation passes unit tests but fails on 15% of production title profiles. Roll back or push forward?+
Answer
Follow-up
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.