Наболевшее про Архитектурные антипаттерны

На днях было у меня одно занимательное собеседование. Я, в общем-то, и не скрываю, что архитектура мне весьма интересна, но, как правило, про нее не особо и говорят на собеседованиях, потому что она, как правило, зашита в какие-либо фреймворки, и в основном именно про фреймворки хотят все поговорить.

Но это собеседование было исключение. Подумал, что я должен обязательно написать про архитектурные антипаттерны.

Архитектурные антипаттерны — это наиболее масштабные и дорогостоящие в исправлении ошибки проектирования. Они могут закладываться на самых разных этапах развития проекта, и возникают они исключительно по вине менеджмента, определяющего вектор развития проекта. Их последствия, как правило, катастрофические — это низкая производительность, невозможность масштабирования и монструозные затраты на изменения. Я лично видел проекты, где сотни разработчиков занимаются не развитием приложения, а поддержкой его в работоспособном положении.

Big Ball of Mud (Большое грязное месиво)

Суть: Это самый распространенный архитектурный антипаттерн. Система не имеет discernible (различимой) архитектуры вообще. Она представляет собой бесформенную массу кода, где всё связано со всем. Это часто является результатом долгой эволюции проекта без рефакторинга, под постоянным давлением сроков.

Причины появления:

  • Постоянные срочные правки и хотфиксы.
  • Отсутствие изначального архитектурного плана.
  • Быстрая смена разработчиков, каждый из которых добавляет код в своем стиле.
  • Принцип «работает — не трогай».

Последствия:

  • Каждое изменение имеет непредсказуемые последствия в других, казалось бы, не связанных частях системы.
  • Невозможно тестировать автоматически.
  • Разработка замедляется в геометрической прогрессии: чем больше кодовая база, тем сложнее и страшнее вносить изменения.
  • Онбординг новых разработчиков крайне затруднен.

Что делать?

  • Постепенное внедрение автоматизированных тестов (начиная с наиболее критичных модулей).
  • Инкрементальный рефакторинг: выделение из “месива” небольших, изолированных сервисов или модулей с четкими API (стратегия Strangler Fig Pattern).
  • Введение строгих code review и стандартов кодирования.

Vendor Lock-in (Привязка к вендору или Карго-культ)

Суть: Архитектура системы настолько сильно завязана на использование конкретных проприетарных технологий, сервисов или библиотек одного поставщика (вендора), что замена этого компонента становится практически невозможной или астрономически дорогой. Строится не столько на принципе технологической необходимости, сколько на слепом доверии вендору либо лицу, отвечающему за архитектуру.

Примеры:

  • Построение всей бизнес-логики вокруг специфических возможностей облачного провайдера (например, использование десятков специфических сервисов AWS, не имеющих аналогов в Google Cloud или Azure).
  • Использование закрытой СУБД с собственным диалектом SQL без абстракции.
  • Сильная зависимость от конкретной библиотеки, которая “зашита” во все слои приложения.

Последствия:

  • Потеря переговорной силы: вендор может повышать цены, а вы не можете уйти.
  • Невозможность миграции на более выгодную или технологичную платформу.
  • Риск прекращения поддержки продукта вендором.

Что делать?

  • Следование принципу “Abstraction over Implementation”: использование абстракций (интерфейсов) для взаимодействия с внешними сервисами.
  • Принцип инверсии зависимостей (DIP): ваш код должен зависеть от абстракций (например, интерфейс IMessageQueue), а не от конкретной реализации (класс AWSSqsService).
  • Использование open-source решений или стандартов (например, SQL вместо проприетарного NoSQL-запроса).

Architecture by Imitation (Архитектура по подражанию)

Суть: Выбор неподходящей, избыточной архитектуры для проекта только потому, что она популярна (модна) или используется крупными компаниями. Классический пример — использование микросервисов там, где достаточно монолита.

Пример:

  • Стартап из 3 разработчиков начинает проект с микросервисной архитектуры в 20 сервисов на Kubernetes, потому что “так делают в Netflix”.
  • Использование сложной распределенной СУБД (Cassandra) для простого сайта-визитки, где хватило бы SQLite.

Последствия:

  • Чудовищная сложность разработки и развертывания (over-engineering).
  • Низкая скорость разработки из-за накладных расходов на поддержку связи между сервисами.
  • Большие операционные затраты на инфраструктуру и DevOps.

Что делать?

  • Выбирать архитектуру, соответствующую масштабу задачи.
  • Начинать с монолита (Monolithic Architecture), а затем, по мере появления реальной необходимости (масштабирование команд, разные требования к нагрузке компонентов), расщеплять его на сервисы.
  • Следовать принципу KISS (Keep It Simple, Stupid).

Database as the Integration Point (База данных как точка интеграции)

Суть: Несколько независимых приложений или сервисов взаимодействуют друг с другом напрямую через общую базу данных. Одно приложение пишет данные в таблицу, а другое читает их оттуда.

Пример:

  • Веб-приложение и фоновый джоб обмениваются данными через таблицу tasks в БД.
  • Два микросервиса используют одну и ту же БД для своих нужд.

Последствия:

  • Жесткая связь (tight coupling): Сервисы не могут эволюционировать независимо. Изменение схемы таблицы одним сервисом ломает все остальные.
  • Невозможность сменить технологию БД для одного из сервисов.
  • Проблемы с производительностью: одна БД становится узким местом для всей системы.
  • Нарушается инкапсуляция данных.

Что делать?

  • Использовать явные API для взаимодействия между сервисами (REST, gRPC, асинхронные сообщения через брокер).
  • Следовать принципу Database per Service в микросервисной архитектуре: каждый сервис владеет своей собственной БД, и только он может к ней обращаться.

Shared Nothing Architecture… gone wrong (Неправильно понятая “Архитектура без общих ресурсов”)

Суть: Идея Shared Nothing Architecture (каждый узел системы независим и самодостаточен) хороша для масштабирования. Но ее неправильное применение приводит к созданию распределенного монолита (Distributed Monolith).

Как это выглядит? У вас есть множество физически разделенных сервисов (микросервисов), но они:

  • Жестко связаны по времени выполнения (синхронные вызовы, образуя длинные цепочки).
  • Имеют общую базу данных или другие общие ресурсы.
  • Должны развертываться вместе из-за тесной зависимости их API.

Последствия:

  • Сочетает в себе худшие черты монолита (жесткая связь, сложность изменений) и микросервисов (сетевая задержка, сложность развертывания и отладки).
  • Отказ одного сервиса вызывает каскадные сбои по всей цепочке.

Что делать?

  • Проектировать сервисы действительно независимыми, с акцентом на асинхронное взаимодействие через сообщения.
  • Следовать принципам устойчивости к сбоям: использовать паттерны Circuit Breker, Retry, Fallback.
  • Внедрять API-шлюзы (API Gateway) и BFF (Backend For Frontend) для изоляции клиентов от внутренней сложной сети сервисов.

Заключение

Как вы поняли, собеседование я не прошел, но я нереально благодарен ребятам за беседу, в результате которой появился этот текст.

И в качестве вывода. Будьте проще. Если вы однажды утром проснулись утром и решили сделать еще один «Озон», только лучше. Делайте. И замонолитьте его только с одной целью — чтоб он у вас был уже вечером. И используйте язык, который вы знаете. И фреймворк, который вы знаете. Именно эти две вещи вам помогут с архитектурой на первом этапе.

А если завтра у вас появились тысячи пользователей и трафик. Начинайте размоноличивать приложение. И чем больше у вас пользователей и трафика, тем больше размоноличивайте. В общем, живите эволюционно, а не революционно и не делайте из еды культа.