За годы работы с TypeScript, Java, Kotlin и Go я не раз сталкивался с одним и тем же паттерном: команда внедряет «правильное» архитектурное решение — и получает не гибкость, а технический долг.
Причина проста: оверинжиниринг маскируется под профессионализм. Мы выбираем сложные инструменты не потому, что они нужны, а потому, что «так делают в серьёзных проектах».
В результате принцип KISS — «Keep It Simple, Stupid» — работает не хуже, чем в 1930-х, когда его впервые сформулировали в среде авиационных инженеров. В этой статье — мой личный список подходов, которые в 90% случаев (особенно в REST-серверах) приносят больше вреда, чем пользы.
Disclaimer
Я не утверждаю, что эти инструменты «плохие». Они оправданы в определённых случаях. Но в типичном бэкенд-проекте (REST API на TypeScript/Java/Go) их часто используют без реальной необходимости. Речь именно об этом.
ORM: когда объекты мешают данным
Объектно-реляционное отображение (ORM) — это мощные библиотеки вроде Hibernate (Java) или TypeORM (TypeScript), которые генерируют SQL-запросы на основе моделей и превращают результаты в объекты. В Java ORM почти неизбежен: язык построен вокруг объектов, а ручная работа с ResultSet (курсорами) неудобна и многословна. В Go ситуация иная — лёгкие инструменты вроде sqlx помогают маппить строки в структуры, но не претендуют на роль «полноценного ORM» и не тянут за собой экосистему зависимостей.
А вот в JavaScript… ORM часто лишён смысла. Запрос к базе и так возвращает массив обычных объектов, а типизировать их в TypeScript можно простым as User[]. В типичном REST-сервере эти данные почти не обрабатываются — их сразу сериализуют в JSON и отправляют клиенту. А если нужны динамические или условные запросы, то тут ORM превращается из помощника в препятствие. Генерация SQL «внутри чёрного ящика» затрудняет отладку, снижает производительность и ограничивает контроль над запросами.
ORM оправдан если сами модели данных (структура таблиц) пластичны. И вряд ли нужен, если вы просто читаете и пишете данные с фиксированной структурой через REST API. В таких случаях лучше взять лёгкий SQL-билдер вроде Knex.js. Он почти дословно повторяет SQL, даёт полный контроль над запросами, работает быстрее и весит в разы меньше: например, заявленный на npm размер Knex в 24 раза меньше, чем TypeORM (на осень 2025-го). Нередко «писать чистый SQL» — это не откат в прошлое, а шаг к здравому смыслу.
Dependency Injection: ради тестов, которых нет
Dependency Injection (DI) — это подход, при котором зависимости (например, репозитории или сервисы) передаются в класс через конструктор, а не создаются внутри или вызываются статически:
// dependency injection
Class UserController {
repo: UserRepo
constructor(repo: UserRepo){ // передаём UserRepo как DI
// и сохраняем как свойство контроллера
this.repo = repo;
}
getUsers(request: HttpRequest){
return this.repo.findAllUsers(request.body as UsersRequest);
}
}
// static
Class UserController {
static getUsers(request: HttpRequest){
// не сохраняем ничего, сразу вызываем статичный метод
return UserRepo.findAllUsers(request.body as UsersRequest);
}
}
DI активно продвигается как «промышленный стандарт», главный аргумент — удобство тестирования: зависимости можно заменить моками.
Однако на практике, юнит-тесты с моками в REST-серверах — редкость. Чаще пишут интеграционные тесты: запускают приложение целиком, подключают тестовую БД и проверяют поведение «от запроса до ответа». В этом случае мокать репозитории не имеет смысла.
Плюс к этому, DI требует инфраструктуры. Чтобы не создавать экземпляры вручную, подключают DI-контейнеры (Spring в Java, NestJS DI в JS). Это наглухо привязывает код к фреймворку, добавляет слои пустых абстракций, делая код неявным.
DI имеет смысл, там где вы действительно мокаете зависимости или имеете взаимозаменяемые реализации (например: разные провайдеры оплаты)
В остальных случаях эта неописуемая красота ничего не добавляет проекту, кроме чувства удовлетворения от ещё одного освоенного паттерна. Вот действительно интересно, какая доля программистов реально что-то мокали в Dependency Injection (мне не удалось ни разу, хотя хотелось).
GUID : глобальная уникальность за счёт читаемости
Отличный способ сгенерировать длинный нечитаемый идентификатор чего угодно. И главное, глобально уникальный — пользователи никогда не пересекутся с аудиофайлами, а файлы с записями к врачу. У этого подхода даже есть свой евангелист — Джефри Рихтер, работавший в Майкрософт. Это уважаемый автор компьютерных книг, а поэтому описывает адекватные способы их использования, связаны они, как сейчас помню, со слиянием корпораций и сведением клиентских баз. Мне ни разу не пришлось столкнуться с такой ситуацией.
А пришлось с тем, что программист (даже и не юный вовсе) всем таблицам в базе назначил идентификаторы в виде GUIDов «потому, что так принято», а кем и почему, он не помнил. Хуже того, даже словари — небольшие списки с простыми записями и с текстовыми уникальными ключами тоже имели ещё и GUIDы и API было построено так, что извлечь полную запись из словаря можно было только по GUIDу, а GUID надо было отдельно выспросить по строковому ключу. Может это очень безопасно? Ну да ладно — это уникальный случай.
GUID имеет смысл при распределённом хранении или создании новых записей, когда нет единого источника идентификаторов (например записи создаются оффлайн, а потом записываются в единое хранилище). В остальных случаях такие ключи просто длинные, нечитаемые, громоздкие и медленные. Не надо их использовать только потому, что у них есть евангелист.
GraphQL: гибкость на которую способен не каждый
Модная альтернатива REST, родившаяся в Facebook. Идея в том, чтобы клиент мог запросить только нужную информацию с необходимыми деталями с помощью унифицированного языка, а сервер сам решит откуда её брать (SQL/noSQL/брокер…) и соединит в один ответ. Вполне разумная идея, если вы Meta и предлагаете свой API кому угодно — т.е. множество разных контрагентов пишет массу разных клиентов к вашему API. Но если, не дай бог, ваша компания единственный автор клиента, то вся эта затея теряет львиную долю смысла — просто не под кого подстраивать запросы. Но даже если вы раздаёте своё API сторонним разработчикам, то GraphQL таит в себе принципиальную опасность — из-за его гибкости, клиенты могут построить запрос неожиданным для вас образом, что приведёт к перегрузкам и подвисаниям. Придётся отдельно ограничивать глубину и сложность запросов и отслеживать их «стоимость». Если в хранении данных есть какие-то узкие места, то они будут замедлять весь интегрированный API.
Так что есть веские причины оставаться в парадигме REST или RPC, где вы сами жестко контролируете, что от куда извлекается и в каком виде — сможете декомпозировать API так, чтобы было удобно не только клиентам, но и серверам. А со стороны клиента сможете распараллелить запросы и отправить их на разные эндпоинты, что визуально может выглядеть «живее» чем долгое ожидание одного «жирного» запроса. Уж точно не стоит начинать свой API с GraphQL – до него проект должен дорасти.
Есть еще несколько популярных приёмов программирования которые излишне усложняют код. Это не технологии или библиотеки, но хотелось бы их упомянуть, хотя бы вскользь.
TRY-CATCHинг: ловим то, что ловить не надо
Частое применение этой конструкции нередко излишне, ведь сам механизм исключений создавался для ситуаций, которых в нормально работающих приложениях не должно быть. Это способ не уронить приложение целиком, если в какой-то его части произошла исключительная ситуация. Либо выдать какое-то предупреждение перед тем, как упасть. Если говорить о REST серверах, то обычно, исключения никак не надо обрабатывать — нужно просто вернуть ответ с ошибкой клиенту. Многие современные фреймворки (Spring Web на Java, NEST и Fastify на JS) позволяют вообще ничего не оборачивать в try-catch — если исключение произойдёт, то фреймворк сам перехватит его, обернёт в объект ошибки и отправит в ответе. Если в формат ошибки нужно внести какие-то изменения, это делается в одном единственном middlware.
Паттерномания: когда инструменты больше, чем здание
Паттерны упрощают сложное, но часто усложняют простое. Они добавляют дополнительные конструкции, чтобы раздробить сложность на меньшие компоненты. А если приложение простое – то дополнительные конструкции присутствуют, а отдельные компоненты становятся настолько примитивными, что просто их наличие вносит сумятицу. Если коллегам приходится тратить дополнительное время на то чтобы понять почему здесь именно фабрика, то может фабрика не нужна вообще?
Гигакомпоненты (фронтенд)
Все вроде знают, что код должен быть связным (отвечать только за одну задачу) и с низким зацеплением (не пользоваться напрямую методами и свойствами других компонентов). В бэкенде это более или менее соблюдается с разной степенью успеха. Но во фронтенде почему-то сплошь и рядом попадаются компоненты на две-две с половиной тысячи строк, в которые засунута вся функциональность какого-нибудь модуля – тут и список, и формы с поиском и фильтрами, и модалы с отдельными сущностями — всё в одном файле. Ещё и запросы к API здесь же. Но на фронтенд распространяются все те же архитектурные принципы, что и на остальное ПО, а известные фронтенд-фреймворки имеют средства как для удобного разделения кода на компоненты, так и для взаимодействия между компонентами. Разумнее делать отдельными компонентами форму поиска/список/модал а взаимодействие организовывать через события и отдельное хранилище.
Итоги
Увеличенная сложность кода — это вроде бы маркер карьерной состоятельности: создаёт иллюзию подкованности, за которую часто платят премиями и уважением. Но на деле это:
замедляет разработку
увеличивает время сборки и деплоя
повышает порог входа для новых разработчиков
создаёт скрытые точки отказа
Простота — это не «недостаток амбиций». Это дисциплина. Мне кажется, выбор простого решения там, где оно достаточно, — признак зрелости и даже известной смелости. Лучше пусть код выглядит «упрощённым», чем будет неподдерживаемым.
PS. А может, это всего лишь личные эстетические предпочтения…
Комментарии (13)

Gabenskiy
21.10.2025 08:15Статья очень сомнительная. Зачем ровнять между собой TS и Java. Для фронта это все может быть и актуально, а я посмотрю, как вы напишете бэк на java без фреймворка. Нет ничего плохого, что мы привязываемся к фреймворку. Не на сервлетах голых же писать? Рынок бэка на джава - это 90% spring, оставшиеся проценты - это микронавт, кваркус, кора. Мигрировать с одного на другое все равно не будет в большом проекте. Поэтому ничего плохого в этом нет.
Далее, если вы юниты не пишете, это не значит, что их никто не пишет. У меня был проект только из интеграционных тестов (95%). Так вот все тесты проходили за 20-25 минут. Очень эффективно.
В некоторых тезисах описание слишком абстрактное, не хватает примеров, из-за этого вообще сложно согласиться или не согласиться с тезисом.
mkant Автор
21.10.2025 08:15Про фронт только один абзац, TS я в основном, использую на бэке. Вы правы - на Java я тоже писал на Spring, и старался отметить (в абзаце про ORM), что на Java его использование не только оправдано, но и удобно. Но это автоматически не распространяется на другой язык. А вот бессмысленное использование DI без моков я встречал и на Java и на TS. И на Java статические репозитории работают так же как и на TS. И описанный случай с GUID был с JAVA программистом — т.е. он настаивал, что именно в Джаве так принято (хотя, конечно, это личное мнение в реальности к Java не имеет никакого отношения, просто привычно, что там всё "солидно" и "промышленно").

Armann
21.10.2025 08:15Возможно вы не поняли про что статья - она не против фреймворков, она против оверинжениринга

lleo_aha
21.10.2025 08:15Вы, возможно, не поняли, что в среднем если людей в рамки паттернов (про фреймворки молчу уж - в js с этим не очень) - то в среднем у них получается лапшичка. Очень фиговая такая, не тестируемая, глючная лапшичка. Код - очень простой при этом. И кривой.

mkant Автор
21.10.2025 08:15Не очень понял формулировки. В JS фронтенд сейчас тотально захвачен фреймворками. Бэкенд на голой ноде видел тоже один раз в жизни, остальное на фреймворках разной степени замороченности. Честно говоря, мне не приходилось видеть глючный, нетестируемый код, который одновременно был бы простым, обычно это ужасающе переусложнённые конструкции, обязательно с несколькими паттернами одновременно.

lleo_aha
21.10.2025 08:15Ну так фреймворк это по сути набор паттернов реализованных. Вот у Вас в статье - "DI это уже оверинж тк тестов все равно нет" - DI не только и не столько про тесты. Без DI народ начинает активно совать везде вызовы чего угодно напрямую - в итоге в приложении модулей как таковых нет - один большой комок, где сервис "оплаты через webmoney" сам лезет в бд за данными заказа и тп.

mkant Автор
21.10.2025 08:15Я старался подать платёжные шлюзы как пример уместного DI. Но кажется, что подключение разных реализаций с одинаковым интерфейсом - это редкий случай, не думаю, что ради этого стоит тащить специальную библиотеку обслуживающую инъекции, можно сделать их вручную - код будет только понятнее.
Фреймворк действительно набор паттернов, и некоторые чувствуют себя обязанным применить их все. Я думаю, вы согласитесь, что это излишне.
"народ .. активно суёт вызовы чего угодно напрямую " - я такое видел на Java на Спринге, конечно DI там тоже был - ничто не способно удержать заядлого кулинара от приготовлении лапши.
Мне кажется, что сам DI не делает код ни понятнее, ни лучше, ни более гибким. Это можно сделать с помощью уместного применения DI, я хотел, чтобы статья была об этом.

SiRanWeb
21.10.2025 08:15Полезная статья, но не согласен с некоторыми доводами.
А вот в JavaScript… ORM часто лишён смысла. Запрос к базе и так возвращает массив обычных объектов, а типизировать их в TypeScript можно простым as User[].
Это работает пока запросы относительно простые. Если запрос делает left join со связью 1:М, хочется иметь массив с объектами вида:
[ { login: 'login1', name: 'name1', comments: [ { text: 'text1', }, { text: 'text2', } ], }, { login: 'login2', name: 'name2', comments: [], }, ]А квери билдеры вернут что-то такое:
[ { login: 'login1', name: 'name1', comment.text: 'text1', }, { login: 'login1', name: 'name1', comment.text: 'text2', }, { login: 'login2', name: 'name2', comment.text: null, } ]Тогда придется вручную мапить, избавляться от дубликатов юзеров и с типизацией разбираться. Согласен, что TypeORM перегружен, но тот же DrizzleORM в 2 раза легче и справляется на ура.
DI активно продвигается как «промышленный стандарт», главный аргумент — удобство тестирования: зависимости можно заменить моками.
Ну не только. DI отлично работает, когда у нас есть несколько сервисов, которым нужно задать одинаковый интерфейс. Например — платежки, которые можно реализовать в виде Стратегии и динамически инжектить.
Чтобы не создавать экземпляры вручную, подключают DI-контейнеры (Spring в Java, NestJS DI в JS). Это наглухо привязывает код к фреймворку, добавляет слои пустых абстракций, делая код неявным.
Вообще никто не заставляет тянуть целый фреймворк. У JS есть куча библиотек типа Awilix, которые не делают ничего, кроме добавления DI-контейнеров.
Многие современные фреймворки (Spring Web на Java, NEST и Fastify на JS) позволяют вообще ничего не оборачивать в try-catch — если исключение произойдёт, то фреймворк сам перехватит его, обернёт в объект ошибки и отправит в ответе.
Да, только не все ошибки мы хотим отдавать клиенту. В простых случаях, мы не будем отдавать исключение упавшего запроса к БД, а выкинем какой-нибудь 500 Internal Server Error, при этом записывая в логи проблемный запрос. В более сложных, сторонние сервисы могут отдавать десятки различных исключений, но пользователю мы хотим вернуть что-то общее, типа "Failed to send email" или "Data not found".

mkant Автор
21.10.2025 08:15вложенные объекты - действительно вариант для ORM. Но, честно говоря, сейчас я так перестал делать. Если запросы разделить (отдельно юзеры, отдельно комменты из вашего примера) то интерфейс реагирует живее. Но если непременно нужно вложенные, то да - ORM, это проще, чем строить запрос с вложенными объектами (что поддерживают не все БД). Может, напишу об этом отдельную статью
про DI с разными реализациями одного интерфейса - согласен. Но я бы делал инъекции вручную - кажется, что такие случаи не настолько всепроникающие, чтобы использовать библиотеку
Ошибки, кажется, тоже проще перехватывать одним мидлваром - и Spring и NEST с Fastify позволяют это делать, именно в нём вы делаете проверки на prod\dev и решаете что именно отдавать клиенту.
В целом у вас совершенно верное восприятие - я не против этих технологий вообще. Я против того, чтобы использовать их там где они профита не дают, а код усложняют

markelov69
Да, всё верно. KISS должен быть всегда превыше всего. Вся остальная сложность должна быть обусловлена лишь сложностью бизнес логики, а не оверинженерингом.