Branch by Abstraction — pattern для refactoring без long-lived branches. Trunk-based dev через interface + feature flag.
Branch by Abstraction нужен, когда большой refactoring нельзя безопасно сделать одним PR и нельзя неделями держать long-lived branch. В реальной системе trunk продолжает жить: команды добавляют features, меняют API, исправляют баги, выкатывают релизы. Чем дольше отдельная ветка с крупной заменой отрезана от main, тем выше цена merge, тем меньше уверенность в поведении и тем страшнее big-bang release.
Паттерн переносит ветвление из Git в runtime-код. Вместо того чтобы иметь отдельную долгую ветку, команда добавляет временный abstraction layer, переводит callers на него, оставляет старую реализацию по умолчанию, добавляет новую реализацию за тем же интерфейсом и переключает трафик feature flag-ом. В main все время лежит рабочая система. Каждый шаг можно задеплоить отдельно, проверить метрики и откатить без массового revert.
Это особенно важно для migrations: замена payment provider, переход на новый SDK, переписывание storage adapter, смена внешнего API, перенос части монолита в новый модуль. Паттерн дисциплинирует: сначала contract, потом постепенное переключение, потом удаление временного слоя.
Mental model: это не branch в Git, а branch в коде. Снаружи callers видят один контракт, например IPaymentProvider. За контрактом некоторое время живут две реализации: old и new. Feature flag решает, куда пойдет конкретный вызов. Когда новая реализация доказала совместимость, старую удаляют, а abstraction либо оставляют как полезный доменный контракт, либо тоже удаляют как временные строительные леса.
Хороший ADR звучит так: мы принимаем временное усложнение кода ради непрерывной интеграции, малого rollback radius и production-проверки на реальном трафике. Мы обязуемся удалить старую реализацию и flag после завершения migration, иначе паттерн превратится в постоянный over-engineering.
От Strangler Fig паттерн отличается уровнем применения. Strangler обычно работает на уровне endpoints, сервисов и маршрутизации вокруг legacy-системы. Branch by Abstraction работает внутри одного codebase: интерфейсы, adapters, dependency injection, feature flags. Их часто комбинируют: сначала BBA внутри сервиса, потом strangler на уровне traffic routing.
Диаграмма показывает три caller-а, abstraction layer с IPaymentProvider interface, две реализации Stripe v1 (old) и Stripe v2 (new), а также feature flag service. В начале есть прямые legacy-связи от callers к old implementation. Затем callers переходят на abstraction, abstraction продолжает ходить в old implementation, а новая реализация появляется behind the same interface.
Физические ребра важны: callers больше не должны знать, какая реализация активна. Они зависят от интерфейса. Feature flag влияет на abstraction, а не размазывается по 40 callers. Это снижает площадь изменений и делает rollback централизованным.
Decision на abstraction фиксирует контекст: миграция со старого SDK на новый занимает недели, long-lived branch приведет к merge hell, а big-bang переключение опасно. Принято решение вести работу через trunk-based development, interface и постепенный rollout.
Step 1: tight coupling показывает исходную боль. Каждый caller вызывает OldImpl.charge() напрямую. Чтобы заменить provider, нужно менять все callers, синхронно согласовывать поведение и выкатывать большой PR. Урок: coupling становится главным риском migration, а не сам новый SDK.
Step 2: introduce IPaymentProvider показывает первый безопасный шаг. Добавляется interface, callers переводятся на него, но за интерфейсом все еще old implementation. Поведение не меняется, значит commit можно отправить в main и production. Урок: первый шаг должен уменьшать coupling без изменения semantics.
Step 3: add NewImpl behind same interface добавляет новую реализацию, но default flag остается old. Код новой реализации уже компилируется, проходит тесты и живет в main, но production-трафик ее не трогает. Урок: интегрировать рано, активировать поздно.
Step 4: feature flag rollout показывает постепенное переключение: 1 процент, 10 процентов, 50 процентов, 100 процентов. Если метрики ухудшаются, rollback — это изменение flag, а не deploy. Урок: deploy и release должны быть разными событиями.
Step 5: cleanup показывает финальную, часто забываемую часть. После недель на 100 процентов new удаляются old implementation, зависимости, flag и лишний abstraction, если он больше не несет доменной пользы. Урок: паттерн завершен только после удаления временного кода.
ADR: Long-lived branch vs Branch by Abstraction. Long-lived branch сохраняет main визуально чище на время работ, но копит integration risk. Branch by Abstraction временно усложняет main, зато каждый шаг проходит CI, code review и production validation. Для trunk-based команд BBA обычно выигрывает, потому что снижает риск финального merge и релиза.
ADR: Interface before implementation vs parallel rewrite. Interface-first подход заставляет явно определить контракт, compatibility tests и границы ответственности. Parallel rewrite без контракта быстрее стартует, но часто заканчивается semantic drift: новая реализация вроде бы делает то же самое, но иначе обрабатывает ошибки, rounding, idempotency или timeouts.
ADR: Runtime feature flag vs compile-time switch. Runtime flag дает canary, rollback без deploy и сегментацию по users или tenants. Но требует системы flags, audit, ownership и cleanup. Compile-time switch проще, но не позволяет безопасно катить процент трафика и быстро откатывать production.
ADR: Keep abstraction vs remove abstraction. Иногда interface становится хорошей доменной границей: платежный provider, object storage, email sender. Тогда его стоит оставить. Но если abstraction была нужна только для migration и больше не защищает от реальной вариативности, ее нужно удалить. Иначе команда платит complexity tax годами.
ADR: BBA vs Strangler Fig. Если замена находится внутри одного deployable artifact, BBA проще. Если нужно заменить внешний сервис, endpoint или часть монолита через routing, лучше Strangler Fig. Если миграция затрагивает и код, и traffic boundary, применяются оба: interface внутри и routing снаружи.
В payment-системах BBA часто используется для перехода между провайдерами или версиями SDK. Callers вызывают доменный контракт charge, refund, authorize, а old/new implementations различаются деталями API. Feature flag позволяет включать нового provider для internal users, малого процента клиентов или отдельных merchant-групп.
В storage migrations похожий подход применяется для перехода с локального filesystem на S3-compatible storage, с одного search engine на другой, с legacy cache client на новый. В database migrations abstraction может закрывать repository или DAO, но тут нужно особенно осторожно следить за transactional semantics и consistency.
Trunk-based development в больших инженерных организациях опирается на такие приемы: small commits, hidden code paths, feature flags, compatibility tests и быстрый cleanup. Continuous Delivery описывает этот стиль как способ делать крупные изменения маленькими production-safe шагами.
Главный anti-pattern — не удалить старую ветку кода. Через полгода никто не знает, какой flag еще нужен, старый provider не тестируется, но его боятся удалить. Для каждого BBA надо заранее создать cleanup issue, owner и критерий завершения: 100 процентов трафика на new, нулевая потребность rollback за N дней, удалены old dependencies.
Вторая ошибка — слишком широкий interface. Если в IPaymentProvider попадает все подряд, новая реализация должна имитировать legacy-детали, которые callers вообще не должны были видеть. Интерфейс должен выражать доменную операцию, а не протекание старого SDK.
Третья ошибка — отсутствие compatibility tests. Старый и новый provider должны проходить общий contract test suite: успешный charge, declined card, timeout, idempotency duplicate, partial failure, refund, rounding, currency handling. Без этого feature flag лишь переносит discovery багов в production.
Четвертая ошибка — флаги без observability. Rollout должен сравнивать error rate, latency, business metrics, retry count, provider response codes и user-visible failures между old и new. Если нет метрик, gradual rollout превращается в медленный blind release.
Пятая ошибка — сделать abstraction глобальным singleton без учета tenants и операций. Для rollback часто нужен routing по user, merchant, request type или geography, а не один флаг на весь мир.
Не используйте Branch by Abstraction для маленького refactoring, который помещается в один понятный PR и не меняет production semantics. Временный interface, flag и две реализации будут лишней стоимостью.
Не используйте паттерн, если невозможно одновременно поддерживать старую и новую реализацию. Например, destructive schema migration без backward compatibility может требовать expand/contract database migration, dual-write или отдельный migration plan до BBA.
Не используйте BBA как оправдание для постоянного plugin-framework. Паттерн про безопасную замену, а не про бесконечную абстрактность. Если вариативность не является реальным бизнес-требованием, cleanup обязателен.
strangler-fig — миграция на уровне сервисов и endpoints.ci-cd-pipeline — почему trunk-based development и зеленый CI важны для BBA.feature-flags — управление rollout, ownership и cleanup.database-migration — expand/contract и backward-compatible schema changes.adapter — когда новая реализация требует протокольного или API-перевода.