BFF (Backend for Frontend): где он реально нужен, а где избыточен
Backend for Frontend появился как ответ на конкретную боль: разные клиенты (веб, iOS, Android, умный холодильник) дёргают одни и те же бекенд-сервисы, но каждому нужен свой формат данных, свой набор полей и своя агрегация. Делать «универсальный» API под всех — он быстро превращается в монстра с тридцатью query-параметрами и разными костылями для каждого клиента.
За двенадцать лет я видел три реализации BFF: одну удачную, одну избыточную и одну, которую через год разобрали обратно. Разберу, в каких контекстах BFF реально окупается, где он создаёт больше проблем, чем решает, и как не превратить его в очередной слой пересылки JSON.
Что такое BFF на самом деле
Сам Сэм Ньюман в исходной статье определял BFF как тонкий слой, специфичный для одного типа клиента: один BFF для веба, другой для мобилки, третий для партнёров. Каждый знает только свою аудиторию и адаптирует под неё ответы общих сервисов.
На практике это значит: BFF не хранит свою БД, не реализует бизнес-логику, не валидирует домен. Он:
- Агрегирует данные из нескольких сервисов в один ответ.
- Преобразует форматы (внутренний JSON в формат, удобный фронту).
- Управляет авторизацией для конкретного клиента (cookie для веба, JWT для мобилки).
- Опционально — кеширует ответы под нужды клиента.
Что BFF не должен делать: содержать бизнес-правила. Если в BFF появляется код вида «если статус заказа Confirmed и пользователь VIP, начислить 10% бонуса» — это знак, что доменная логика утекла из ядра.
Когда BFF окупается
Я выделяю несколько признаков, при которых BFF реально решает проблему.
Сильно разные клиенты
Веб-страница хочет десять полей с пагинацией по 20 элементов и подгруженными деталями автора. Мобильное приложение — три поля с пагинацией по 50 и без деталей автора (там их рендерит другой экран). Smart TV — два поля и кеш на час.
Если запихать всё в один /api/items, у вас появится два варианта плохого: либо overfetching (мобилка качает в 3 раза больше нужного), либо разные query-параметры с условной логикой. Через полгода этот эндпоинт никто не понимает.
BFF здесь даёт каждому клиенту свой ответ ровно того формата, что ему нужен. Веб-BFF возвращает агрегированный объект, mobile-BFF — урезанный, TV-BFF — прокешированный.
Несколько источников за один экран
Главный экран приложения собирает данные из трёх сервисов: профиль пользователя, лента событий, баланс. Делать три запроса с фронта — три раундтрипа, медленный first paint. Делать один запрос на бекенд, который параллельно дёргает три сервиса — ровно задача BFF.
@RestController
class MobileHomeBff(
private val users: UserClient,
private val feed: FeedClient,
private val billing: BillingClient
) {
@GetMapping("/m/home")
suspend fun home(@AuthenticationPrincipal user: User): HomeResponse = coroutineScope {
val profile = async { users.profile(user.id) }
val events = async { feed.recent(user.id, limit = 20) }
val balance = async { billing.balance(user.id) }
HomeResponse(
displayName = profile.await().name,
avatar = profile.await().avatarUrl,
balanceLabel = formatBalance(balance.await()),
events = events.await().map { it.toMobileDto() }
)
}
}Один запрос с мобилки, три параллельных вызова на бекенде, один склеенный ответ. Здесь BFF честно зарабатывает свой хлеб.
Команды разделены по платформам
Веб-команда хочет деплоить новую фичу прямо сегодня. Мобильная команда не зарелизится раньше следующего цикла App Store, две недели. Если оба зависят от одного API, веб ждёт мобилку или мобилка ломается.
BFF на платформу даёт автономию: web-BFF меняется веб-командой, mobile-BFF — мобильной. Ядро остаётся стабильным, изменения — в тонком слое адаптации.
Когда BFF — лишний слой
Теперь о случаях, когда BFF добавляет сложность, не давая ничего взамен.
Один клиент
Если у вас только веб, а мобилки и партнёров не планируется в обозримом будущем, отдельный BFF — это лишний хоп. Раздавайте API напрямую с тех же сервисов или с одного API gateway.
Аргумент «вдруг появятся новые клиенты» — слабый. BFF легко добавить, когда он понадобится. До этого момента он стоит работы и infrastructure.
Тонкая обёртка на одно поле
BFF, который только пересылает запросы один-в-один в нижестоящий сервис, — это анемичный прокси. Я видел проекты, где для каждого эндпоинта нижестоящего API заводили зеркальный эндпоинт в BFF, без агрегации, без трансформации, просто «чтобы был свой».
Через полгода BFF превращается в тысячу эндпоинтов, каждый из которых — функция passthrough(). Нет никакого выигрыша, есть удвоенная работа на изменение и удвоенная зона багов.
Микрофронт с разной семантикой
Несколько микрофронтов на одном веб-сайте, каждый со своими данными — кажется, что нужен BFF на каждый. Иногда — да, но чаще достаточно одного веб-BFF с разными модулями внутри. Заводить пять BFF под пять микрофронтов — это попытка повторить микросервисную архитектуру там, где она не нужна.
BFF vs API gateway
Часто путают эти штуки. Они разные.
API gateway — общий вход в систему. Маршрутизация, авторизация, rate limiting, логирование, общий слой безопасности. Один на всю организацию.
BFF — слой адаптации под конкретного клиента. Один на тип клиента. Может стоять за gateway или вместо него.
На практике конфигурации бывают такие:
- Только gateway: ноль адаптации под клиента, все API общие.
- Только BFF: gateway-функции дублируются в каждом BFF, обычно через общую библиотеку.
- Gateway + BFF: gateway делает общую безопасность, BFF — клиент-специфику.
Для команд от 50 разработчиков и нескольких продуктов комбинация gateway + BFF обычно выигрывает: общие концерны не дублируются, специфика клиента изолирована.
Кто владеет BFF
Самый частый организационный вопрос. Я считаю: BFF принадлежит фронт-команде. Это её инструмент адаптации, и она должна иметь право его менять без согласований.
Если BFF владеет бекенд-команда, появляется бутылочное горлышко: «нам нужно поле, бекендщики добавят через две недели». Тогда BFF теряет половину смысла — его как раз делают, чтобы фронту не зависеть от темпа бекенда.
Стек BFF — тоже фронт-стек. Веб-BFF на Node.js (часто с тем же языком, что и фронт), mobile-BFF — может быть на чём угодно, но команда выбирает то, что знает. Я видел Node, Kotlin, Go — все рабочие, главное, чтобы команда могла поддерживать.
Подводные камни
Несколько граблей, на которые я наступал.
BFF становится монолитом. Команда фронта добавляет логику в BFF, потом ещё и ещё. Через год в BFF — половина бизнес-правил, и нижестоящие сервисы ходят к нему за справками. Это инверсия архитектуры. Лекарство — code review с явным правилом: бизнес-логика в BFF — повод спросить «почему здесь, а не в core».
Дублирование между BFF. Веб и mobile BFF делают одну и ту же агрегацию, в обоих живёт одинаковый код «получить профиль и расширить балансом». Решение — общая библиотека/сервис домена, к которой BFF обращаются. Не копипаста.
BFF теряет надёжность. Один из трёх параллельных запросов упал — что вернуть? Полный 500? Частичный ответ с пометкой? Падение нижнего сервиса не должно ронять весь экран. Я обычно реализую graceful degradation: тайм-аут на источник, при таймауте — placeholder в ответе.
val balance = async {
try {
withTimeout(500) { billing.balance(user.id) }
} catch (e: Exception) {
log.warn("balance fallback for user=${user.id}", e)
null
}
}Версионирование контракта. Веб-BFF меняет ответ — старая версия мобильного приложения не понимает (у мобилки другой BFF, но если они шарят DTO — привет). Каждый BFF должен иметь независимый контракт со своим клиентом. Не шарить DTO между mobile и web BFF, даже если структура похожа.
Тестирование. BFF — это место, где сходится много сервисов. Юнит-тесты тут малополезны. Нужны контрактные тесты с нижестоящими и e2e-тесты с фронтом. Pact или аналог — почти обязательно.
BFF и GraphQL
Часто слышу: «зачем BFF, если есть GraphQL — клиент сам выбирает поля». Частично справедливо. GraphQL решает проблему overfetching и агрегации одного запроса в одно место.
Но GraphQL не решает: разную авторизацию для разных клиентов, разную логику кеширования, специфику форматов (mobile хочет ISO-даты, TV хочет Unix timestamp). Эти задачи всё равно где-то решаются, и BFF — естественное место.
В моей практике хорошо работает связка: GraphQL-шлюз для гибких запросов от веба, отдельный REST BFF для мобильного приложения, где нужна жёсткая фиксация контракта. Не каждому клиенту подходит GraphQL.
Что запомнить
BFF — инструмент для адаптации под клиента, не серебряная пуля. Он окупается, когда у вас несколько клиентов с разными потребностями, или когда экран собирается из нескольких сервисов. Он избыточен, когда клиент один или когда он превращается в анемичный прокси.
Главное — не пускать в BFF бизнес-логику. Тонкий, клиент-специфичный, владеется фронт-командой, разваливается грациозно при сбое нижестоящих. Если хотя бы одно из этих свойств теряется, пора посмотреть, что у вас на самом деле получилось — может, это уже не BFF, а ещё один ядерный сервис, который нужно правильно назвать.