zero deps
57 subscribers
1 photo
27 links
Node.js, V8, highload, архитектура.
Где производительность, а где оверинжиниринг?

Код, профилирование и воспроизводимые бенчмарки.

Всё, что ставим — потом и поддерживаем.
Фиксим то, что сами и сломали.

zerodeps.tech
Download Telegram
Channel created
npm install — это RCE

Каждый раз, когда пишем npm install, мы запускаем чужой код:
в деве, в CI, в проде. И почти никогда не читаем, что там внутри.

npm устроен так, что одна зависимость может притянуть сотни.
Где на любом уровне — postinstall, бэкдор, телеметрия или просто баг.

Добавляя “удобный” хелпер (`deepClone`, `dateFormat`), можно получить зависимости, у которых на любом уровне может быть:
postinstall, выполняющий shell-скрипт
prepare с подменой файлов перед публикацией
– динамический импорт из внешнего URL
– замаскированный base64/pako payload
– инициализация, собирающая ENV, hostname и proxy-данные

Или просто:
– устаревший код, который тормозит рантайм
– сломанный код при следующем миноре

Многие пакеты не имеют CI, не проходят аудит.
package-lock
не спасёт, если у тебя ^ или обновление в dev-зависимости.

Не так давно были заражены:
eslint-config-prettier, eslint-plugin-prettier, @pkgr/core.

Сотни тысяч проектов могли получить трояна через обычный npm install.

Каждый npm install — это акт доверия.
Если ты не читаешь, что ставишь — ты запускаешь произвольный код.

Чем ниже уровень — тем выше цена.

Zero Deps — это не мода. Это контроль.
👍3👨‍💻32
WebSocket не нужен. Почти всегда.

WebSocket создаёт иллюзию простоты:
постоянное соединение, двусторонняя связь, никакой лишней обёртки.
Но на практике это тяжёлый протокол и почти всегда избыточный.

Постоянное соединение — не бесплатное.

Каждое соединение — это:
– выделенный TCP-сокет
– память на буферы и таймеры
– регулярные ping/pong пакеты
– необходимость держать пользовательский state на сервере

При 100k подключениях в idle:
– память: 300–500 МБ (`Node.js`, `uWebSockets.js`)
– CPU: ~7% просто на поддержание соединений
– одна массовая рассылка может триггерить GC и просадку latency

Масштабирование WebSocket — отдельная боль:
– Sticky-сессии обязательны (иначе нельзя писать адресно)
– Горизонтальное масштабирование требует внешнего pub/sub
– Протокол не работает через CDN
– Никаких встроенных ack, retry, QoS

В большинстве задач WebSocket не нужен вообще.
Альтернативы проще, стабильнее, масштабируются лучше:

EventSource (SSE)
Односторонний канал server → client. HTTP/1.1, простой протокол, балансируется обычным nginx. Подходит для realtime фидов, алертов, обновлений UI.

Long polling
Рабочий вариант. HTTP-запрос держится открытым до появления данных. Поддерживает edge-кеш, не требует сложной инфраструктуры.

Push через REST + CDN
Если данные можно кешировать даже на 1–2 секунды — дешевле отправить через pull-модель. REST + Cloudflare/Fastly + max-age — и клиент почти в real-time.

MQTT / raw TCP
Когда реально нужен duplex и контроль — лучше уйти на уровень ниже. Там можно сделать свой протокол с ack, QoS, batching.

WebSocket — это не default.
Это инструмент для узкого класса задач:
двусторонняя синхронизация, RTC, редакторы, игры.

Во всех остальных случаях он добавляет ресурсоёмкость, сложность и архитектурные ограничения. Без реальной пользы.

Если можно не держать соединение — не держи.
4👍4🔥2
Мысли GPT

Раньше я спорил с собой. Сейчас чаще — с GPT.
Иногда он формулирует неплохие мысли.

Highload — не про RPS.

Это про поведение системы на пределе.
Пики запросов сами по себе ни о чём, если не видно, что в этот момент творится с CPU, памятью, диском, сетью, очередями.

Важно не просто «держать удар», а понимать — за счёт чего.

Где система начинает деградировать?
Есть ли backpressure?
Как быстро сбои эскалируются?
Что происходит, когда ресурсы заканчиваются: сбрасываем, замедляемся или падаем?

Настоящий highload начинается там, где масштабирование уже не спасает.

И выигрывают не те, у кого больше инстансов.
А те, кто знает, как ведёт себя их код под нагрузкой.
3👍2🤔1
JSON — не для highload
Просто потому что он не был для этого задуман.


Формат JSON — компромисс между читаемостью и универсальностью.
Но как только он оказывается в hot-path — на входе, в кэше, в ответе — он начинает течь.

Что не так:

- JSON.stringify — дорого: рекурсивный обход, аллокации, кастомные .toJSON(), ошибки на BigInt.
- JSON.parse — не потоковый, всегда аллоцирует всё сразу.
- Без схемы — нет совместимости, всё проверяется вручную.
- Нет стриминга — только весь объект целиком.
- Типы примитивны — всё превращается в строки и числа.
- Производительность непредсказуемая, особенно на глубоко вложенных структурах.

Вывод:

JSON — это про обмен между людьми и машинами.
Highload — это обмен между системами.

Нужны схемы, бинарные форматы, стриминг и предсказуемость.

JSON туда не лезет. Его туда тянут.


Альтернатива: fast-json-stringify
Компилирует сериализацию по JSON Schema. Без магии, без рекурсии, без сюрпризов.

Но лучше без JSON, zero deps же
👍31
Почему встроенный http — всё ещё лучший выбор для Node.js-сервера

Многие до сих пор тянут Express, Fastify и похожие обёртки. Но если ты сам пишешь системный код, думаешь про производительность и безопасность — разумнее остаться на стандартном http.

Плюсы встроенного модуля:

– ноль зависимостей;
– полное управление поведением, без скрытой логики;
– работа напрямую с сокетом;
– стабильный API с Node.js 0.x;
– читаемый, отлаживаемый и без сюрпризов.

Каждый фреймворк добавляет слои. Иногда они удобны. Но чаще — маскируют простую задачу под “современный подход”, добавляют неявные правила, просадки производительности, боттлнеки и риски от зависимостей.
Некоторые навязывают архитектурные паттерны, даже когда задача — просто перекладывать JSON.

Если ты не пишешь pet-проект, а отвечаешь за критичный сервис —
import { createServer } from 'node:http'
— это всё, что тебе нужно.
3👍2
Zero deps — не религия, а привычка проверять

Любая npm-зависимость — это:
- чужой код, который запускается у тебя (иногда сразу через postinstall);
- десятки транзитивных пакетов, о которых ты даже не знаешь;
- риск: от уязвимостей и бэкдоров до банального “сломано”.

Но zero deps — не про страх. Это про привычку.

Что значит zero deps-подход на практике:
1. Если можешь — не ставь. Особенно ради одной функции.
2. Если ставишь — изучи. На чём держится? Легко ли заменить?
3. Если сомневаешься — напиши своё. Примитив? Да. Зато твой.

Zero deps — не догма, а здравый осознанный подход.
Это про поддержку, прозрачность и контроль.

Если ты не знаешь, что именно запускаешь — это не твой код.
5👍2
Архитектура без фетишей

Видел в проектах:

– IUserUseCase - UserServiceImpl - UserRepository - PgUserAdapter
– UserAggregate, UserEntity, UserDto, UserModel — всё про одного юзера
– CommandBus, EventBus, QueryHandler — в CRUD-е на три метода

Вместо рабочего кода — культ архитектуры.

Что модно (и часто бесполезно):

Clean Architecture — 4 слоя прокладок. Половина — просто return next().
DDD — доменная модель ради модели. Предметки нет, названия остались.
Hexagonal — “порты/адаптеры”, но один порт, один адаптер, всегда вместе.
CQRS — два класса вместо одного. Зато “правильно”.
Event-driven всё — даже если можно было вызвать функцию.
SOLID — звучит строго, но на практике усложняет там, где не нужно.

Что реально работает:
DRY - один раз написал, используешь, где нужно.
KISS - чем проще, тем лучше.
SRP — код делает одну вещь.
DIP — зависимости подменяемы, логика изолирована.
Явные зависимости — без сервис-локаторов и “магии”.
Простая структура — transport → logic → storage.
Меньше абстракций — проще читать, проще менять.

Не пиши код для Дяди Боба. Пиши для следующего инженера.
Не строй архитектуру как диплом. Строй так, чтобы не было стыдно жить с этим через полгода.


Архитектура — не манифест. Это способ не получить стрелу в колено через месяц.
3👍2
Где начинается архитектура?

Часто под архитектурой подразумевают нечто крупное: диаграммы, сервисы, базы, очереди.
Но это лишь один из уровней.
На самом деле архитектура пронизывает всё: от названия переменной до устройства экосистемы из сотен сервисов.


Уровень 1: переменная
Если ты называешь переменную data, tmp или того хуже a, b, ты вносишь неопределённость.
Это снижает предсказуемость, повышает когнитивную нагрузку, увеличивает шанс ошибки.
Даже такие мелочи могут иметь архитектурные последствия.

Уровень 2: функция
Функция, которая делает несколько вещей, — антипаттерн, антиархитектура.
Плохая декомпозиция = невозможность покрыть тестами, переиспользовать, локализовать баг.
А потом эту функцию копируют, и дефект масштабируется. Это уже "вирусная" архитектура.
Чистая функция — там, где это возможно — хорошее правило.

Уровень 3: модуль (или класс)
Где границы? Что знает о внешнем мире? Где side-effect, где чистая логика?
Если всё перемешано — модуль становится токсичным. Появляется архитектурный долг.
Про SOLID можно вспомнить, но слепо следовать — не стоит. Это не закон, а инструмент.

Уровень 4: приложение
Здесь начинается архитектура, о которой все говорят: деплой, отказоустойчивость, масштабирование.
И здесь же локальные решения начинают конфликтовать между собой.

Вопросы:
- Где entry point и как устроен lifecycle?
- Разделены ли core и инфраструктура?
- API отделён от логики?
- Есть ли единый контейнер зависимостей?
- Поддерживается ли graceful shutdown?
- Что происходит под нагрузкой?

Если у тебя всё завязано на index.mjs — это скрипт, не система.
Если каждый модуль сам себе открывает базу — ты не управляешь ничем.

Уровень 5: сервисы и экосистема
Здесь архитектура становится явной.
Ты больше не один. Каждый модуль, каждая точка отказа — это чья-то зона ответственности.

Вопросы:
- Как сервисы общаются? Протоколы, форматы, контракты.
- Кто владеет данными и отвечает за консистентность?
- Что делать при недоступности одного из сервисов?
- Как живут запросы, проходящие через 3–5 сервисов?
- Есть ли ретраи, деградация, алерты?

Если сервис лезет в чужую базу — это не архитектура, а хаос.
Если один отказ тянет каскад — это не система, это стек домино.


Принцип
Архитектура — не про масштаб, а про осознанность.
Каждое локальное решение влияет на уровень выше.
Если ты не проектируешь явно — ты проектируешь по умолчанию. И, скорее всего, плохо.


Вывод
Архитектура начинается не на доске.
Она начинается с вопросов:

А можно ли тут сделать чистую функцию?
А читаемый ли код?
А если это упадёт — что произойдёт?


Если ты задаёшь эти вопросы — ты уже в процессе.
Если нет — никакие блок-схемы не помогут.

И да: плохое имя переменной может быть началом деградации.
Так же как чёткое понимание точек отказа — началом зрелости.
👍4🤔21
0. Что даёт TypeScript, кроме типов

Вопрос не праздный. Типы — это главное, но не единственное, что приносит TS в проект.

Вот что ещё он навязывает:

Препроцессор

TS — это не просто типы, это много синтаксического сахара поверх JS.

Он требует компиляции. Всё, что ты пишешь — не исполняется напрямую.
Даже const x = 1 as const — это синтаксис, который нужно разобрать и превратить обратно в JS.

Именно поэтому:
- нужна настройка tsconfig.json;
- нужен сборщик (tsc / swc / esbuild);
- нужен transpile target (ES2022? CommonJS?);
- нужна настройка импорта

Неочевидные runtime-эффекты

TS-фичи, которые не имеют смысла в runtime, но могут влиять на поведение:
- декораторы;
- emitDecoratorMetadata;
- import type, export type;
- const enum (внезапно исчезает в скомпилированном коде);
- namespace'ы (исчезают полностью, но могут конфликтовать при объединении файлов).

Если не знаешь, как это трансформируется — можно получить странный и не эффективный JS.

Ограничения по фичам языка

TS тормозит внедрение новых JS-фич:
- не всегда поддерживает новый синтаксис сразу;
- вынуждает писать в стиле, удобном для трансформации, а не читаемости;
- часто требует даунгрейда target (ради Node-совместимости);
- декларации типов для новых API появляются с задержкой.

Ты не можешь использовать свежий JS "как есть". Всё должно пройти через типовой компилятор.
Чем ближе ты к нативному JS — тем сильнее TS мешает.

TS становится точкой централизованной сложности

Любая сложность в сборке, типах, импортах, конфигурации — в итоге сводится к TypeScript:
- непонятный moduleResolution;
- конфликт версий @types/...;
- магия paths и baseUrl;
- ESM + TS + Jest + Babel = боль.

Ты начинаешь тратить время не на разработку, а на отладку toolchain'а.

Что имеем в итоге?

TypeScript — это не просто типы.
Это целый инструментальный стек, который требует, чтобы ты думал в его терминах.

Иногда это оправдано. Чаще — нет.

Если всё, что тебе нужно — это типы, можно обойтись JSDoc'ом.
Без компиляции, без лишнего runtime, без скрытого препроцессинга.
👍43
1. TypeScript не гарантирует надёжность

TypeScript часто подают как инструмент «защиты от ошибок».
Но это не защита. Это компилятор, который верит тебе на слово.

Он проверяет типы только в пределах того, что ты сам описал.

Он не валидирует входные данные. Не ловит runtime-ошибки. Не гарантирует корректность логики.

Надежность типизации ограничена:
• если API возвращает не то — компилятор не узнает;
• если ты неверно понял схему — типы будут ложными;
• если прилетели некорректные данные — баг всё равно произойдёт;
• если хочешь обойти систему — as unknown as, @ts-expect-error, any и поехали.

TypeScript не мешает ошибаться — он просто делает это чище.

Пример:
//Описал тип
type User = { id: string, email: string }

//Ответ пришел
// { "id": 42, "email": null }

// Используешь тип
const user = response as User

// И компилятор тебе поверит.
// До первого user.email.toLowerCase()


Если не валидировать вход — получаешь баг на ровном месте.

А дальше выбор:
• тянуть рантайм-библиотеку (zod, io-ts);
• писать собственный инструмент для проверки;
• или руками валидировать каждый объект.

Возникает вопрос: какой ценой мы получаем мнимое ощущение безопасности при возможном несоответствии типов?

TypeScript — это не про надежность и «безопасность».

Это про удобство разработки внутри доверенного кода.

Без валидации на границах системы типы ничего не гарантируют.
В системе безопасность начинается с входных данных.
Типы — уже потом.


Другие части:

0. Что даёт TypeScript, кроме типов
👍31🔥1
2. TypeScript ломает zero-deps и замедляет цикл разработки

Компиляция — это цена. Даже если ты её не замечаешь.

Для запуска TS-кода тебе нужно:
- tsconfig.json;
- транспиляция (tsc, swc, esbuild);
- типы зависимостей (@types/...);
- поддержка ESM или CommonJS;
- линтер с типами;
- конфиг для тестов, алиасов, runtime-импорта.

Каждый шаг — точка отказа. Любое несовпадение версий, любые нестыковки — и пайплайн падает.

Zero-deps становится невозможен: язык требует инфраструктуру.

Даже с Node.js v23.6.0, где TypeScript встроен на уровне рантайма, остаётся всё то же: конфигурация, типы, source maps, IDE-интеграция.

Это не «убрали сборку». Это встроили другой сборщик. Бремя осталось.

Чистый JS запускается сразу.
TS — только после обвязки.


Дольше цикл разработки


TypeScript ломает быстрый цикл:

написал → запустил → исправил → повторил.

Становится:
написал → собрал → подправил конфиг → проверил типы → запустил → проверил пути → дебажил через мапы → исправил → пересобрал.

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

Ухудшение DX

Developer Experience — это не «удобно писать код в IDE».
Это: быстро читать, быстро запускать, быстро менять.

С TS:
- IDE может не видеть типы до полной сборки;
- tsc может упасть из-за файла, к которому ты не прикасался;
- линтеры требуют доп. плагинов и синхронизации с tsconfig;
- ошибки появляются там, где их нет на runtime.

TS вставляет прослойку между мыслью и результатом.
JS — работает напрямую.

Отладка — через боль


TypeScript ломает прозрачность рантайма:
- source maps не всегда точные;
- стеки уводят в dist/, не в исходник;
- console.log работает не там, где ты пишешь;
- hot reload требует костылей;
- для запуска кода из ./src тебе нужно убедиться, что ./dist актуален.

Тормозит командную работ

Любой merge может:
- поломать сборку;
- привести к конфликту типов;
- зацепить @types/..., которые подтянулись транзитивно;
- потребовать пересогласования alias.

Проект перестаёт быть “просто нодой”.
Он превращается в конвейер, который надо всё время чинить

Что в итоге?

TypeScript даёт мнимую стабильность на длинном горизонте.
Но за это ты платишь: скоростью, простотой, прозрачностью.

Если ты пишешь system-level tooling, оркестратор, сервер или CLI — TypeScript может быть не помощником, а помехой.

Чем ближе к ядру — тем важнее zero-deps.
И тем дороже каждая секунда, потерянная на сборку.

Другие части:

0. Что даёт TypeScript, кроме типов
1. TypeScript не гарантирует надёжность
3👍2
Когда абстракция начинает мешать

Иногда код перестаёт быть инструментом и превращается в лабиринт.
Это происходит, когда абстракция, задуманная для упрощения, начинает скрывать важные детали и тормозить изменения.

Как это заметить:
- Чтобы изменить одну строчку логики, ты бродишь по трём-четырём слоям кода.
- Больше времени уходит на понимание связей, чем на саму задачу.
- В тестах приходится подменять половину проекта моками, потому что абстракции закрыли доступ к данным.
- Новому разработчику сложно объяснить архитектуру без многоуровневой диаграммы.

Абстракция — это контракт.

Это упрощённое представление сложной системы, которое скрывает детали реализации и оставляет только то, что нужно для использования.  
Хорошая абстракция помогает работать с кодом на более высоком уровне, не вникая каждый раз в низкоуровневую реализацию.

Она полезна, пока:
1. Снижает когнитивную нагрузку.
2. Не мешает отладке.
3. Не ломает принцип локальности изменений (когда правка затрагивает минимум мест в коде).

Когда абстракция нарушает хотя бы два пункта, она превращается в технический долг.

Чеклист: вредная абстракция

Если хотя бы 3 ответа «да» — пора упрощать:
-  Описание абстракции занимает больше одного предложения и включает «и ещё…»?
-  Для отладки нужно читать её внутренний код?
-  Неясно, где вход и где выход данных?
-  Изменение бизнес-логики затрагивает больше трёх файлов?
-  Для тестирования требуются сложные моки или фейки?
-  Новому разработчику нужно больше часа, чтобы понять, как её использовать?

Пример 1:
Вместо прямого вызова db.query(...) вы сделали три уровня: ORM + репозиторий + сервис. На старте это казалось «правильной архитектурой». Через год — для добавления нового поля в запрос вам приходится лезть в мапперы, DTO, конвертеры и тесты для каждой прослойки. Любая правка тянет за собой десяток файлов.

Пример 2:
Вместо простого HTTP-клиента для внешнего API сделали обёртку с «универсальным» интерфейсом, планируя поддержку разных провайдеров. Но новых провайдеров так и не появилось, а теперь при отладке запроса нужно разбираться в слоях адаптеров, трансформеров и middleware, которые в итоге лишь дублируют логику оригинального SDK.

Что в итоге?
Если абстракция усложняет отладку, замедляет изменения и заставляет править много несвязанных мест, её нужно упростить или удалить.

Хорошая абстракция ускоряет работу, плохая — тормозит.


Еще интересное про абстракции:
Закон дырявых абстракций - почему абстракции «протекают» и когда это важно.

Sebastian Markbåge “Minimal API Surface Area” - как лишние фичи мешают эволюции кода

Sandi Metz The Wrong Abstraction - как распознать «не ту» абстракцию и безопасно её распрямить.
3👍2🔥2
3. Когда TypeScript оправдан

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

Когда TS имеет смысл:

- Длинная жизнь продукта и кодовая база, которая растёт годами.
- Несколько команд, параллельные изменения, обилие внутренних и внешних API.
- Есть публичные SDK/клиенты, где обратная совместимость — не пожелание, а требование.
- Стоимость рантайм-ошибки высока: деньги, SLA, безопасность.

Если проект маленький, недолговечный, с узкими интерфейсами — издержки типизации часто превышают пользу.

Что даёт TS:

- Контракты на границах модулей. Явные типы для DTO, событий, адаптеров. Ломаете поле — компилятор показывает фронт разрушений.
- Дешёвый рефакторинг. Переименование, выделение интерфейсов, смена сигнатур — IDE и компилятор ведут тебя за руку.
- Локализация неопределённости. unknown/never/asserts подчёркивают места, где риск и где нужны проверки.
- Документация, которая не врёт. Типы — это живой, проверяемый артефакт, а не устаревающий README.

Чего TS не даёт:

- Архитектуры. Границы контекстов, потоки данных, изоляция побочных эффектов — это решения уровня дизайна.
- Согласованности данных и эволюции схем. Миграции, инварианты и порядок обновления сервисов — за вами.
- Надёжности. Повторные попытки, идемпотентность, дедупликация событий — это протоколы и инфраструктура.
- Производительности. Типы не ускоряют обработку запросов и не уменьшают GC-паузы.

Формула простая: TS снижает стоимость изменения, но не подменяет мышцу архитектурного мышления.

Пример:

Рефакторинг поля total в amount в DTO между сервисами orders и billing:

С TS практически сразу будет обозначен фронт проблем во всех местах, где читали total, включая сериализацию, агрегации и тесты. Без TS часть мест «проскочит», всплывёт в проде при редком кейсе.

Контрпример:

В одном модуле сетевая логика, доменная модель и кэш. TS это оттипизирует, но не разрежет связность. По-прежнему сложно независимо тестировать и разворачивать части. TS помогает согласовать и облегчить рефакторинг, но не исправит архитектуру.

Что в итоге?

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

Если проект небольшой — строгая дисциплина модулей и простые соглашения дадут такой же эффект с меньшими накладными расходами.

Другие части:

0. Что даёт TypeScript, кроме типов
1. TypeScript не гарантирует надёжность
2. TypeScript ломает zero-deps и замедляет цикл разработки
3👍2
Визуализация архитектуры через C4 Model

C4 — модель визуализации архитектуры, предложенная Саймоном Брауном.
Её задача — дать единый способ описания системы на разных уровнях детализации, так чтобы и бизнес, и разработчики, и эксплуатация видели одну и ту же архитектуру, но с нужной им степенью глубины.

Вместо одной перегруженной диаграммы C4 предлагает четыре отдельных представления:

1. Context
Показывает систему в окружении: кто её использует, какие внешние сервисы есть, как идёт взаимодействие. Это уровень, на котором важны роли пользователей, интеграции и точки входа.

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

3. Component
Внутреннее устройство контейнера: ключевые модули, их обязанности и связи. Это взгляд разработчика на систему, когда важно понять, в каком модуле живёт логика и как она связана с другими частями.

4. Code
Детализация до уровня классов и функций. В отличие от UML, в C4 этот уровень опционален — его используют только там, где реально полезно держать карту реализации.

Чем полезна модель:
- Делит архитектурное описание на понятные и управляемые слои.
- Даёт общий язык для обсуждения — от бизнеса до инженеров.
- Упрощает актуализацию документации: можно обновить один уровень, не переписывая всё.
- Работает одинаково для монолитов, распределённых систем и микросервисов.

Что важно понимать:
- C4 — это формат описания, а не метод проектирования. Он не подскажет, как правильно разделить сервисы или где ставить границы.
- Модель фиксирует уже принятые решения. Если решение плохое, на диаграмме оно просто будет аккуратно нарисовано.
- Она хорошо работает только при регулярном обновлении. Устаревшая диаграмма хуже, чем её отсутствие.


Пример применения:
Для новой системы можно сделать Context-диаграмму для стейкхолдеров, Container-уровень для DevOps и команды разработки, а Component-уровень — для бэкендеров, которые будут поддерживать сервис.
При этом все уровни связаны: элемент на Context-уровне соответствует контейнеру на следующем, а контейнер — набору компонентов внутри.


Полезное:
https://c4model.com/
👍53
C4 Model и "уровни" архитектуры

В прошлом посте я рассказал про C4 модель Саймона Брауна. Модель помогает визуализировать архитектуру так, чтобы и бизнес, и инженеры, и эксплуатация видели одну и ту же картину, но с нужной детализацией.

А так же я писал про то Где начинает архитектура - от переменной до экосистемы.

Если присмотреться то, можно заметить, что модель C4 довольно неплохо ложиться на эти уровни.

Давай посмотрим:

Context - Уровень 5
Диаграмма контекста в C4 — это взгляд на систему в окружении: пользователи, внешние сервисы, интеграции. Это соответствует экосистемному уровню: кто с кем говорит, какие протоколы, какие точки отказа.

Container - Уровень 4
Контейнеры — это крупные исполняемые части: сервисы, базы, очереди, фронтенды. Здесь важны вопросы развертывания, деплоя, масштабирования, согласованности и отказоустойчивости. Это соответствует архитектуре приложения: entry point, разделение core-логики и инфраструктуры, сценарии деградации.

Component - Уровень 3
Компоненты — модули внутри контейнера. Это уже граница между архитектурой и проектированием модулей: что модуль знает о внешнем мире, где side-effect, где бизнесс логика.

Code - Уровень 2–1
C4 опционально доходит до уровня классов и функций, но это уже зона проектирования кода. Тут работает дисциплина имен, SRP, локализация ответственности — то, что в описании на уровне переменной и функции.

Что в итоге?

C4 неплохой инструмент для согласования картины системы между разными уровнями участников. Но он закрывает только вопрос как показать архитектуру.

Для ответа на вопрос: какой она должна быть — всё те же принципы: чёткие границы, минимальные зависимости, предсказуемое поведение при сбоях.

Полезное:
Simon Brown — Visualising Software Architecture
Architectural boundaries
👍42
Зачем ты везде тащишь очереди?

Вот ты проектируешь свою систему, базы, микросервисы, кэш - все красиво. И вот встает вопрос: как это все связать, как они будет общаться?

И тут со всех утюгов кричат:
- У тебя микросервисы - тебе нужны очереди!
- Очереди - это модно.
- Очереди - это полезно.
- Очереди - это масштаб и надежность.
- Очереди "корпоративный стандарт". (как и TS, ага да)

Ты берешь, допустим RabitMQ, поднимаешь брокер (+1 сервис и глобальная точка отказа), и получаешь большую асинхронную боль сквозь всю систему, рост когнитивной нагрузки и эксплуатационные издержки: лаг, дубликаты, краш консумера, ядовитые сообщения и дрейф схем. Да есть и полезное: буферизация, распределение по времени, изоляция сбоев и фан-аут. Но какой ценой?

Если у тебя CRUD между двумя сервисами и одинаковые требования по времени ответа и доступности - очередь только навредит.

Синхронный вызов честно говорит: «либо сделал, либо нет».
С очередью ты говоришь: «когда-нибудь, возможно, кто-то где-то это обработает».

Отсюда вылезают взрослые темы, которые почему-то игнорируют, когда «прикручивают Kafka/Rabbit ради моды»:
- Доставка: at-least-once - дубликаты - идемпотентность на приёмнике
- Порядок: забудь «строгий порядок», у тебя head-of-line blocking или вообще все перемешано.
- Нагрузка: «очередь поможет удержать пик нагрузки» — да, но хвост растёт. Хвост — это твой скрытый долг по времени отклика.
- Мониторинг: трассировка через брокер — это чёрный ящик и пляски с кореляторами.
- Контракты: cобытие — это публичное API. Версионирование, эволюция схем, миграции потребителей — отдельная боль.

Очередь приносит тебе асинхронность как обязательство. А это обязательство принуждает тебя проектировать идемпотентные операции, хранить входящие/исходящие события, жанглировать ретраями и дедупликацией, думать про повторный прогон истории (пересчёт из журнала). Если ты к этому не готов - будет больно, очередь сломает прод, или тебя.
🔥3
Пример

Сценарий: Создать задачу и отправить ее на исполнение в другой сервис.

Вариант А — MongoDB + outbox (без внешней очереди):

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

javascript
// Сервис "Планирователь"
/**
* @param {import('mongodb').MongoClient} client
* @param {import('mongodb').Db} db
* @param {Task} task
* @returns {{status: boolean, taskId: string}}
*/
export async function createTask (client, db, task) {
const session = client.startSession()

try {
const eventId = crypto.randomUUID()

session.startTransaction()

await db.collection('tasks').insertOne(
{
_id: task.id,
type: task.type,
payload: task.payload,
status: 'pending',
createdAt: Date.now()
},
{ session }
)

await db.collection('outbox').insertOne(
{
_id: eventId,
type: 'task.created',
payload: { taskId: task.id },
createdAt: Date.now(),
pickedAt: null,
doneAt: null
},
{ session }
)

await session.commitTransaction()

return { status: true, eventId }
} catch (error) {
console.error(`[createTask] ${task.id}:`, error)
await session.abortTransaction()
} finally {
await session.endSession()
}
}

// Сервис "Делатель"
/**
* @param {import('mongodb').MongoClient} client
* @param {import('mongodb').Db} db
* @returns {Promise<void>}
*/
export async function executor (client, db) {
// атомарно «забронировали» событие
const event = await db
.collection('outbox')
.findOneAndUpdate(
{ type: 'task.created', pickedAt: null },
{ $set: { pickedAt: Date.now() } },
{ sort: { createdAt: 1 }, returnDocument: 'after' }
)

if (!event) {
//нет событий
delay(100)
return
}

// идемпотентность на уровне обработчика
const processedId = `${event._id}:executor`
const processed = await db.collection('processed').findOne({ _id: processedId })

if (!processed) {
// уже делали — пропускаем
return
}

const task = await db.collection('tasks').findOne({ _id: event.payload.taskId, status: { $ne: 'done' } })

if (!task) {
// таск уже кто-то сделал
return
}

if (task) {
try {
// делаем таску, помним про идемпотентность
await doWork(task)
await db.collection('processed').insertOne({ _id: processedId, at: Date.now() })
await db.collection('tasks').updateOne({ _id: task._id }, { $set: { status: 'done' } })
await db.collection('outbox').updateOne({ _id: event._id }, { $set: { doneAt: Date.now() } })
} catch (error) {
// ретраи с backoff, лимит → DLQ
console.error(`[executor] ${task.id}:`, error)
await db.collection('outbox').updateOne({ _id: event._id }, { $set: { pickedAt: null } })
await db.collection('processed').deleteOne({ _id: processedId })
await delay(100)
}
}
}
// executor запускается setInterval или setTimeout выше по архитектуре

В примере важное: Задача и событие родились в одном коммите — значит, не потеряются. Исполнитель читает из той же БД — меньше инфраструктуры, меньше точек отказа. Цена — ты сам хозяин ретраев и DLQ, но это как раз то, что нужно контролировать.

Уже слышу крики: "А как же изоляция, границы, связность, какой "ужос" два сервиса работают с одной бд. Как же Чистая архитектура?!" Нормально, отвечу я, когда два сервиса читают одну БД: один владеет записью и инвариантами, остальные читают только стабильную проекцию/реплику по контракту — вот и вся изоляция. Ну это мы отвлеклись. Продолжим.
👍3🔥3