За пределами вайбкодинга: фундаментальная база программной инженерии
В последние пару лет порог входа в разработку изменился навсегда. С появлением Cursor, Claude Code, GitHub Copilot и больших языковых моделей родился феномен «вайбкодинга» (vibe coding): ты набрасываешь мысли в чат на естественном языке, ловишь ощущение потока, нейросеть за секунды генерирует компоненты, скрипты, роуты API — и все это действительно запускается в браузере.
Для пет‑проектов, прототипов на хакатонах и быстрой проверки продуктовых гипотез это потрясающий инструмент. Спорить с этим глупо: скорость черновой сборки выросла в разы. Но у этой легкости есть опасная цена: иллюзия, что генерация синтаксически корректного кода — это и есть разработка программного обеспечения.
В реальности код — лишь самый дешевый и поверхностный артефакт. Настоящая программная инженерия (Software Engineering) начинается там, где сгенерированный код встречается с суровой физической действительностью:
-
с памятью, системными вызовами и блокировками ввода‑вывода операционной системы;
-
с конкурентными транзакциями, гонками данных и аномалиями в базах данных;
-
с сетевыми задержками, каскадными сбоями и отсутствием идемпотентности при повторах;
-
с уязвимостями бизнес‑логики (IDOR/BOLA), утечками секретов и лицензионной чистотой;
-
с наблюдаемостью (Observability), когда упавший в два часа ночи сервис нужно диагностировать не по
console.log, а по трейсам и метрикам.
Когда проект выходит за рамки изолированного локального демо, вайбкод без понимания фундамента превращается в карточный домик: сервис ложится под сотней одновременных пользователей, балансы уходят в минус из‑за состояния гонки, а очередь запросов блокирует воркеры намертво. Нейросеть генерирует код, но архитектуру, инварианты отказоустойчивости и модели данных за нее обязан держать человек.
Ниже — структурированное инженерное руководство, опирающееся на признанную классику индустрии (Мартин Клеппман, Майкл Найгард, практики Site Reliability Engineering Google, Рэндал Брайант и Дэвид О’Халларон). Здесь разобраны пять ключевых пластов, которые отделяют оператора промптов от системного инженера.
1. Железо, ОС, память и ввод‑вывод: почему код тормозит и падает в OOM
Ловушка вайбкодинга: блокировка Event Loop и утечка дескрипторов
Нейросеть часто пишет код в однопоточных асинхронных рантаймах с циклом событий (Node.js, Python asyncio / FastAPI) так, будто ресурсы процессора и операционной системы бесконечны:
// Типичный вайбкод: синхронный парсинг гигантского JSON или вызов тяжелой криптографии прямо в обработчике HTTP-запроса
app.post('/import', async (req, res) => {
const data = fs.readFileSync(req.body.filePath); // Блокирует поток ОС!
const parsed = JSON.parse(data); // Блокирует Event Loop на секунды!
for (const item of parsed) {
fs.createReadStream(item.file); // Дескрипторы не закрываются при ошибке!
}
res.json({ status: 'ok' });
});
На локальном ноутбуке с файлом в 10 килобайт код летает. В продакшене прилетает файл на 500 мегабайт: один такой запрос намертво вешает весь однопоточный цикл событий (Event Loop), сервер перестает отвечать на health‑check запросы балансировщика, а тысячи незакрытых файловых потоков приводят к ошибке EMFILE: too many open files.
1.1. Пространство ядра, пространство пользователя и системные вызовы
Любая современная ОС разделяет память на два изолированных мира:
-
User Space (пространство пользователя): изолированная песочница, где работают прикладные процессы (ваш Node.js, Python, PostgreSQL). Процесс не имеет прямого доступа к железу.
-
Kernel Space (пространство ядра): привилегированный режим, управляющий физической памятью, планировщиком потоков, сетевыми интерфейсами и дисками.
Любое действие с внешним миром — чтение с диска, отправка пакета в сокет, выделение новой страницы памяти — требует системного вызова (syscall: read, write, epoll_wait). Системный вызов — дорогая операция: процессор обязан сохранить регистры процесса, переключить контекст из пространства пользователя в ядро, выполнить валидацию прав, передать данные и переключиться обратно. Код, который в цикле делает миллион мелких записей в файл вместо буферизованной записи, тратит 90% времени не на полезную работу, а на постоянное переключение контекста процессора.
1.2. Модели ввода‑вывода: Блокирующий I/O против epoll и io_uring
-
Блокирующий ввод‑вывод (Blocking I/O): поток делает системный вызов
read()из сокета и засыпает. Ядро убирает поток из очереди на выполнение, пока сетевая карта не получит пакеты. Если клиентов десять тысяч, классической моделиодин поток на соединение(Thread‑per‑Connection) конец: каждый поток съедает от 1 до 8 МБ стековой памяти, а планировщик ОС задыхается в переключениях контекста. -
Неблокирующий ввод‑вывод и мультиплексирование (Non‑blocking I/O, epoll / kqueue): один рабочий поток регистрирует тысячи сокетов в ядре через системный вызов
epoll_ctlи вызываетepoll_wait. Ядро будит поток только тогда, когда в конкретном сокете появились байты (модель уведомления о готовности). На этом держится реактивный Event Loop в Node.js, Nginx, Redis и Netty. При этом в Linux классическийepollработает для сетевых сокетов и пайпов, но не поддерживает обычные дисковые файлы — именно поэтому рантаймы вроде Node.js (libuv) вынуждены читать файлы через отдельный фоновый пул потоков ОС. -
Современный асинхронный интерфейс ядра: io_uring: новейший механизм Linux решает проблему радикально через проакторную модель. Ядро и процесс разделяют две кольцевые очереди в общей памяти (Submission Queue и Completion Queue). Процесс отправляет пачки операций чтения/записи (и сетевых, и дисковых) и забирает результаты вообще без системных вызовов в режиме поллинга ядра (
IORING_SETUP_SQPOLL). -
Среда выполнения Go: язык Go абстрагирует эту сложность на уровне собственного рантайма. Его планировщик
мультиплексирует миллионы легковесных горутин на системные потоки ОС, а сетевые вызовы прозрачно паркуются через внутренний сетевой поллер (network poller на базе epoll/kqueue), не блокируя поток операционной системы.
-
Инженерное правило: в однопоточных циклах событий любая синхронная вычислительная задача (
проход по массиву, тяжелое регулярное выражение с катастрофическим возвратом ReDoS, парсинг большого JSON) полностью замораживает обработку всех сетевых клиентов. Тяжелые вычисления обязаны выноситься в пулы воркеров (Worker Threads / отдельный фоновый процесс очереди).
1.3. Виртуальная память, Page Faults и сборка мусора
Память, которую видит программа, не является физическими планками RAM:
-
Процессор и MMU (Memory Management Unit) работают через виртуальную память, разбитую на страницы (обычно по 4 КБ).
-
Когда программа запрашивает память (
malloc/ создание объекта), ОС не выделяет физическую память мгновенно: она резервирует виртуальный диапазон. Лишь при первой реальной записи в эту область процессор ловит аппаратное прерывание Page Fault, и ядро связывает виртуальную страницу с физическим фреймом. -
Утечки памяти и сборка мусора (Garbage Collection):
-
Вайбкодеры наивно полагают, что в языках с GC (JS, Python, Go, Java) утечек памяти не бывает. Это опасное заблуждение. GC освобождает память только тогда, когда на объект нет ссылок от корней достижимости (GC Roots).
-
Глобальный кэш без TTL и лимита размера, забытые слушатели событий (
emitter.on), замыкания в долгоживущих скоупах — ссылки остаются живыми вечно. Память процесса (RSS — Resident Set Size) растет, ОС начинает сбрасывать неактивные страницы в SWAP на диск (дичайшие тормоза), после чего процесс безжалостно убивает Linux OOM Killer. -
Паузы Stop‑the‑World: чем больше куча (heap) забита миллионами мелких объектов, тем дольше сборщик мусора обходит граф ссылок, вызывая спайки задержек (Latency Spikes) на сотни миллисекунд.
-
2. Данные, типы, индексы и транзакции: как не потерять деньги
Ловушка вайбкодинга: списание баланса и потерянные обновления (Lost Update)
Классический код от нейросети для списания средств или бронирования билета выглядит так:
// Наивный вайбкод: катастрофа конкурентного доступа
async function withdrawMoney(userId: string, amount: number) {
const user = await db.user.findUnique({ where: { id: userId } });
if (user.balance >= amount) {
// В этот момент параллельный запрос делает то же самое!
const newBalance = user.balance - amount;
await db.user.update({
where: { id: userId },
data: { balance: newBalance }
});
}
}
Если отправить два параллельных запроса на списание 100 рублей при балансе 100 рублей, оба запроса одновременно прочитают баланс 100, оба пройдут проверку balance >= amount, и оба запишут баланс 0. Пользователь списал 200 рублей при наличии 100. Это каноническая аномалия Lost Update (потерянное обновление).
2.1. Алгоритмическая сложность в коде: O(N) против O(1) и N+1 в ORM
-
Скрытая квадратичная сложность
: Вайбкодер часто пишет лаконичные цепочки методов массивов, не задумываясь о том, что происходит под капотом:
// Каждая итерация users.filter вызывает линейный поиск orders.some -> сложность O(N * M) const activeUsers = users.filter(user => orders.some(order => order.userId === user.id) );При 10 000 пользователей и 10 000 заказов этот код выполнит
итераций и подвесит процессор на несколько секунд. Инженерное решение: предварительно собрать
SetилиMapидентификаторов заи затем выполнить фильтрацию за
.
-
Проблема N+1 запросов в ORM: Нейросеть генерирует удобный код: загрузить 100 постов, а затем в цикле обратиться к
post.author.name. ORM делает 1 запрос за списком постов и затем 100 отдельных SQL‑запросов за каждым автором. База данных захлебывается сетевым оверхедом. Решение: жадная загрузка (Eager Loading черезJOIN/include), либо батчинг через паттерн DataLoader.
2.2. Типобезопасность на границах: IEEE 754, UTC и схемы валидации
-
Плавающая точка IEEE 754: В двоичном представлении чисел с плавающей точкой (
float,double,numberв JS) дробные доли вроде0.1являются бесконечными периодическими дробями:0.1 + 0.2 === 0.30000000000000004Хранить или вычислять финансовые балансы во float категорически запрещено. Финансы считаются либо в целых числах в минимальных неделимых единицах (копейки, центы, сатоши) черезBigInt, либо в строгом типе с фиксированной точностью (DECIMAL / NUMERICв SQL). -
Временные зоны и даты: Никогда не храните в базе данных локальное время без таймзоны. Все даты во внутренней логике и хранилище — строго UTC в формате ISO 8601 (например,
2026-11-05T11:27:39Z) либо Unix Epoch Timestamp в миллисекундах. Смещение таймзоны клиента применяется только на самом последнем этапе визуализации в UI. -
Схемы на границах системы (Schema Validation): TypeScript проверяет типы только на этапе компиляции. В рантайме входящий JSON от пользователя или стороннего API — это нетипизированная грязь. Инженер валидирует каждый входящий байт через строгие рантайм‑схемы (Zod, Pydantic, TypeBox) до того, как данные попадут в доменную логику.
2.3. Индексы в базах данных: B‑Tree, Hash, GIN
База данных без индексов при любом запросе с WHERE выполняет Sequential Scan (полное сканирование таблицы): считывает с диска в память каждый блок таблицы. На 1000 строк это незаметно, на 10 миллионах — запрос выполняется десятки секунд.
-
B‑Tree (B‑дерево): дефолтный индекс для большинства СУБД (PostgreSQL, MySQL). Сбалансированное дерево поиска с высокой степенью ветвления, оптимизированное под чтение блоками диска. Идеально для точечного поиска за
, поиска по диапазонам (
WHERE age BETWEEN 20 AND 30) и сортировки (ORDER BY). -
Hash‑индекс: хранит 32-битный хеш от значения колонки. Работает исключительно на строгое равенство (
=), не поддерживает поиск по диапазонам и сортировку, но может быть компактнее и быстрее B‑Tree при поиске по длинным строковым ключам (длинным URL, токенам, хешам SHA-256). -
GIN (Generalized Inverted Index): инвертированный индекс. Карта, где каждому элементу (слову в полнотекстовом поиске или ключу в JSONB) сопоставлен список строк, где он встречается. Незаменим для массивов и сложных JSONB‑структур.
-
Цена индексов: каждый индекс — это отдельная структура на диске, которую СУБД обязана синхронно перестраивать при каждой операции
INSERT,UPDATEиDELETE. Навесить 15 индексов на активно обновляемую таблицу — верный способ убить производительность записи.
2.4. ACID, уровни изоляции и блокировки
-
ACID:
-
Atomicity (Атомарность): транзакция либо выполняется целиком, либо не оставляет следов.
-
Consistency (Согласованность): транзакция переводит БД из одного валидного состояния в другое с соблюдением всех инвариантов (foreign keys, check constraints).
-
Isolation (Изоляция): параллельные транзакции не должны ломать данные друг друга.
-
Durability (Долговечность): если транзакция закоммичена, ее результат не пропадет даже при внезапном отключении питания сервера (гарантируется записью в журнал упреждающей записи WAL — Write‑Ahead Logging).
-
-
Аномалии конкурентного доступа:
-
Dirty Read (Грязное чтение): транзакция А читает данные, измененные транзакцией Б, которая затем откатывается (Rollback).
-
Non‑repeatable Read (Неповторяющееся чтение): транзакция А дважды читает одну строку, но между чтениями транзакция Б изменяет ее и коммитится.
-
Phantom Read (Фантомное чтение): транзакция А запрашивает диапазон строк, а транзакция Б вставляет новую строку, подходящую под условие.
-
-
Уровни изоляции транзакций (SQL Standard):
Read Uncommitted -> Read Committed (дефолт в PostgreSQL) -> Repeatable Read -> Serializable. Чем выше уровень изоляции, тем ниже параллелизм и выше риск получить ошибкуSerialization Failure. -
Решение проблемы Lost Update:
-
Пессимистичная блокировка (Pessimistic Locking):
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;Блокирует строку на уровне СУБД до завершения текущей транзакции. Другие транзакции ждут своей очереди. -
Оптимистичная блокировка (Optimistic Locking): В таблицу добавляется колонка версии:
UPDATE accounts SET balance = balance - 100, version = version + 1 WHERE id = 1 AND version = 5;Если параллельная транзакция успела изменить строку первой, условиеversion = 5не сработает, запрос обновит 0 строк, и приложение вернет управляемую ошибку повторной попытки. -
Атомарные операции в SQL:
UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100;
-
3. Сети и распределенные системы: таймауты, идемпотентность и сбои
Ловушка вайбкодинга: голый fetch и двойные списания
Вайбкодер пишет сетевой вызов так, будто сеть надежна:
// Опасный вайбкод: зависание пула соединений и дублирование операций
async function processPayment(orderId: string) {
// 1. Нет таймаута! Если платежный шлюз зависнет, промис будет висеть вечно
const response = await fetch('https://payment-gateway/charge', {
method: 'POST',
body: JSON.stringify({ orderId, amount: 500 })
});
// 2. Наивный ретрай: если ответ отвалился по таймауту, но шлюз деньги снял,
// повторный вызов снимет деньги второй раз!
}
3.1. Заблуждения о распределенных вычислениях (Fallacies of Distributed Computing)
Еще в 1990-х годах Питер Дойч и Джеймс Гослинг сформулировали 8 ложных предположений, в которые верят начинающие разработчики:
-
Сеть надежна.
-
Задержка равна нулю.
-
Пропускная способность бесконечна.
-
Сеть безопасна (слепое доверие к трафику внутри периметра без mTLS и концепции Zero Trust).
-
Топология сети неизменна.
-
В сети только один администратор (на практике у внешних API, облаков и магистральных провайдеров разные владельцы и политики маршрутизации).
-
Транспортные расходы равны нулю.
-
Сеть однородна.
В распределенной системе сеть гарантированно упадет, пакеты потеряются, а задержки вырастут. Проектировать систему нужно с расчетом на постоянные частичные отказы (Partial Failures).
3.2. Паттерны устойчивости по Майклу Найгарду (Release It!)
-
Таймауты (Timeouts): каждый сетевой вызов обязан иметь жесткий тайм‑аут (Connection Timeout и Read/Socket Timeout). Без таймаута один подвисший внешний сервис съест все свободные потоки вашего пула, вызвав каскадное падение (Cascading Failure) всей вашей инфраструктуры.
-
Экспоненциальная задержка с джиттером (Exponential Backoff + Jitter): Повторять упавший запрос сразу же — верный способ устроить атаку типа «отказ в обслуживании» (Self‑inflicted DDoS) на восстанавливающийся сервис. Задержка между повторами должна расти экспоненциально (
секунд) с добавлением случайного шума (Jitter):
sleep_time = min(max_delay, base * 2^attempt) + random_jitterДжиттер размазывает пиковые нагрузки сотен клиентов во времени, предотвращая проблему «стадного эффекта» (Thundering Herd Problem). -
Автоматический выключатель (Circuit Breaker): Паттерн изолирует сбоящий сервис. Имеет три состояния:
-
Closed (замкнут): вызовы идут штатно.
-
Open (разомкнут): если процент ошибок превысил порог, выключатель размыкается и мгновенно возвращает локальную ошибку (Fast Fail), даже не пытаясь слать сетевые запросы к мертвому сервису.
-
Half‑Open (полуразомкнут): по истечении времени таймера пропускает пробный запрос для проверки восстановления зависимости.
-
3.3. Идемпотентность и ключи идемпотентности (Idempotency Keys)
В сетях существует три семантики доставки сообщений:
-
At‑most‑once (не более одного раза): сообщения могут теряться, но дублей нет.
-
At‑least‑once (не менее одного раза): сообщения не теряются, но неизбежны дубли из‑за ретраев.
-
Exactly‑once (строго один раз): физически недостижима в чистом виде в распределенных системах без распределенных транзакций и идемпотентности на стороне приемника.
Идемпотентная операция — операция, многократное выполнение которой приводит к тому же результату, что и однократное ().
-
Методы HTTP
GET,PUT,DELETEпо спецификации обязаны быть идемпотентными. МетодPOSTидемпотентным не является. -
Idempotency‑Key: клиент генерирует UUID операции и передает его в заголовке
Idempotency-Key. Сервер сохраняет ключ в транзакции с результатом операции. При повторном запросе с тем же ключом сервер возвращает уже сохраненный ответ из кэша/БД, не выполняя бизнес‑логику повторно. -
Защита от гонки самих ретраев: запись с ключом идемпотентности захватывается в БД со статусом
IN_PROGRESSдо начала выполнения бизнес‑логики. Если параллельный дубль прилетит раньше, чем завершился первый запрос (например, клиент отвалился по короткому таймауту и тут же повторил вызов), сервер вернет статус409 Conflict(или дождется завершения первого через блокировку), не допуская параллельного исполнения одной и той же операции.
3.4. Теорема CAP, PACELC и паттерн Transactional Outbox
-
Теорема CAP (Эрик Брюэр): при сетевом разделении (Partition Tolerance
), которое в физических сетях неизбежно, система может выбрать только одно:
-
Либо согласованность (Consistency
): все узлы отдают строго одинаковые последние данные, либо возвращают ошибку.
-
Либо доступность (Availability
): любой здоровый узел возвращает ответ, но данные могут быть устаревшими.
-
-
Теорема PACELC (Даниэль Абади): расширяет теорему CAP на штатный режим работы: если произошел разрыв сети (Partition), система выбирает между доступностью (Availability) и согласованностью (Consistency); но иначе (Else), когда сеть работает исправно, мы все равно вынуждены выбирать между низкой задержкой ответа (Latency) и строгой согласованностью (Consistency), так как синхронная репликация на несколько удаленных узлов физически требует времени.
-
Проблема двойной записи (Dual‑Write Problem):
// Классическая ошибка: попытка записать в БД и отправить сообщение в очередь await db.order.create(order); await kafka.send('order-created', order); // Если здесь сбой сети, заказ в БД есть, а события нет! -
Паттерн Transactional Outbox: Событие не отправляется в брокер сообщений напрямую. Оно сохраняется в ту же базу данных в специальную таблицу
outboxв рамках единой локальной транзакции ACID вместе с заказом. Отдельный фоновый процесс (Debezium через чтение WAL‑логов или простой поллер) считывает таблицуoutboxи гарантированно доставляет события в Kafka/RabbitMQ с семантикой at‑least‑once.
4. Безопасность и лицензии в эпоху генеративного кода
Ловушка вайбкодинга: уязвимости BOLA/IDOR и галлюцинации зависимостей
Нейросети на удивление хорошо пишут функциональную логику, но систематически игнорируют контекст авторизации и цепочки поставок:
// Опасный вайбкод: Broken Object Level Authorization (IDOR)
app.get('/api/orders/:id', authenticateJWT, async (req, res) => {
// Код проверил, что юзер залогинен, но НЕ проверил, принадлежит ли заказ этому юзеру!
const order = await db.order.findUnique({ where: { id: req.params.id } });
res.json(order);
});
Любой авторизованный пользователь может перебрать чужие идентификаторы заказов в URL и выкачать всю базу данных конкурентов.
4.1. Главные уязвимости веб‑приложений (OWASP Top 10)
-
BOLA / IDOR (Broken Object Level Authorization): уязвимость номер 1 в современных API. Любой доступ к ресурсу по идентификатору обязан содержать проверку владения:
SELECT * FROM orders WHERE id = :id AND user_id = :current_user_id -
Инъекции (SQL / Command Injection): конкатенация пользовательского ввода в тело SQL‑запроса или вызов shell‑команд через
exec(cmd + input)— гарантированный взлом. Использовать можно только параметризованные запросы (Prepared Statements), где пользовательский ввод передается в движок БД отдельно от дерева синтаксического анализа SQL. -
Утечки секретов и CORS:
-
Хранение API‑ключей в клиентском SPA‑коде (React/Vue бандлы). Все, что попало в браузер, публично доступно любому человеку через
DevTools -> Sources. -
CORS (Cross‑Origin Resource Sharing) — это защита браузера пользователя от вредоносных сайтов, а не защита вашего сервера от запросов через curl или Postman.
-
Типичная уязвимость от LLM: когда разработчик просит нейросеть «починить ошибку CORS», та часто генерирует динамическое зеркалирование присланного заголовка Origin (настройка
origin: trueилиorigin: req.headers.originв пакете cors) в паре сAccess-Control-Allow-Credentials: true. Браузеры по спецификации Fetch блокируют комбинациюAccess-Control-Allow-Origin: *с учетными данными, но динамическое эхо присланного origin обходит эту проверку, открывая сессионные куки и приватные API жертвы для любого произвольного домена в интернете.
-
4.2. Безопасное хранение секретов и сессий
-
Хранение паролей: алгоритмы SHA-256 или MD5 не подходят для паролей, так как они создавались для быстрого вычисления контрольных сумм. Видеокарта перебирает миллиарды SHA-256 в секунду. Пароли хешируются специализированными алгоритмами с параметрами задержки памяти и времени: Argon2id (победитель Password Hashing Competition) либо bcrypt / scrypt.
-
Хранение JWT: хранить чувствительные токены доступа в
localStorageопасно — любая XSS‑уязвимость (внедрение стороннего скрипта через уязвимую зависимость) позволяет украсть токен. Токены сессий передаются в cookies с флагамиHttpOnly(JS не имеет доступа к куке),Secure(только по HTTPS) иSameSite=Strict/Lax(защита от CSRF).
4.3. Лицензионные риски и атаки Slopsquatting
-
LGPL (Lesser General Public License): В отличие от жесткого GPL, LGPL позволяет динамически линковать библиотеку с закрытым проприетарным кодом без необходимости открывать исходники всего вашего проекта. Но если вы статически скомпилируете код или внесете правки в саму LGPL‑библиотеку, эти изменения обязаны быть опубликованы.
-
Атаки на цепочки поставок через AI: Slopsquatting: Нейросети склонны галлюцинировать несуществующие названия пакетов (например, придумывают удобный пакет
fast-jwt-validator). Злоумышленники анализируют галлюцинации моделей, регистрируют эти имена в публичных реестрах (npm, PyPI) и вшивают туда трояны. Вайбкодер бездумно копирует командуnpm install fast-jwt-validatorиз чата с LLM и добровольно заражает свой сервер. Инженерное правило: строгая фиксация хэшей зависимостей через Lockfiles (package-lock.json,poetry.lock), использование приватных прокси‑репозиториев и автоматический аудит уязвимостей (Snyk, Trivy, Dependabot).
5. Архитектура, тестирование и наблюдаемость (Observability)
Ловушка вайбкодинга: ком грязи и отладка принтами
Когда приложение генерируется промптами по частям, через месяц кодовая база превращается в Big Ball of Mud (большой ком грязи):
-
SQL‑запросы размазаны прямо внутри UI‑компонентов или контроллеров.
-
Любое изменение в одном месте ломает пять не связанных с ним модулей (высокая связность — Tight Coupling).
-
При возникновении ошибки в продакшене вайбкодер вставляет
console.log('HERE 1'), перезапускает сервис и пытается угадать проблему.
5.1. Управление связностью: Cohesion, Coupling и чистая архитектура
-
Зацепление (Cohesion): степень, в которой элементы внутри одного модуля логически связаны друг с другом. Модуль должен решать одну задачу (Single Responsibility).
-
Связность (Coupling): степень зависимости модулей друг от друга. Цель архитектора — Low Coupling (слабая связность).
-
Инверсия зависимостей (Dependency Inversion, D из SOLID): Высокоуровневые модули бизнес‑логики не должны зависеть от низкоуровневых адаптеров (конкретной СУБД, конкретной платежки, файловой системы). Оба уровня должны зависеть от абстракций (интерфейсов/контрактов). Практика: доменный сервис списания баланса зависит от интерфейса
PaymentGateway. В тестах туда подставляетсяMockPaymentGateway, в продакшене —StripePaymentGateway. Код бизнес‑логики не меняется ни на строчку.
5.2. Визуализация архитектуры: C4 Model Саймона Брауна
Вместо устаревших блок‑схем 1990-х годов современная программная инженерия использует модель C4 (c4model.com) — масштабируемую карту архитектуры системы по аналогии с Google Maps:
-
Context (Контекст): верхний уровень. Кто пользователи системы и с какими внешними корпоративными системами она интегрируется.
-
Containers (Контейнеры): отдельно развертываемые единицы. Single Page App в браузере, API на Go, база данных PostgreSQL, очередь Redis.
-
Components (Компоненты): внутреннее устройство контейнера. Контроллеры, сервисы бизнес‑логики, репозитории данных.
-
Code (Код): диаграммы классов или интерфейсов (нужны крайне редко, только для самых сложных алгоритмических узлов).
При этом фундаментальные архитектурные решения фиксируются прямо в репозитории короткими документами — ADR (Architecture Decision Records): контекст проблемы, рассмотренные альтернативы и выбранный компромисс.
5.3. Верификация в эпоху генеративного кода: пирамида тестирования и Property‑Based тесты
Если нейросеть пишет код со скоростью 500 строк в минуту, узким горлышком разработки становится не написание кода, а доказательство его корректности:
-
Пирамида тестирования:
-
Unit‑тесты: быстрые тесты чистых функций и доменных инвариантов без сети и БД.
-
Integration‑тесты: проверка реального взаимодействия с базой данных и брокером (запускаются в изолированных Docker‑контейнерах через Testcontainers).
-
End‑to‑End (E2E): сквозные тесты критических пользовательских путей.
-
-
Тестирование на основе свойств (Property‑Based Testing — QuickCheck, Fast‑Check, Hypothesis): Вайбкодер пишет 2–3 позитивных unit‑теста на те примеры, которые придумал сам. Property‑based тесты проверяют математические инварианты системы на десятках тысяч сгенерированных псевдослучайных граничных входных данных (пустые строки, нулевые байты, отрицательные числа, максимальный
MAX_INT, строки Unicode со смайликами и иероглифами). Это лучший способ отловить галлюцинации LLM.
5.4. Наблюдаемость (Observability) по стандартам Google SRE
Отладка современных распределенных систем строится на трех столпах телеметрии:
-
Структурированное логирование: никаких неструктурированных строк
print('User error'). Логи пишутся в строгом формате JSON с единым набором обязательных полей (timestamp,level,service_name,trace_id,user_id,error_stack). Такие логи легко парсятся и индексируются в ClickHouse/Elasticsearch. -
Метрики и перцентили:
-
Измерять производительность через среднее арифметическое (Average / Mean) — профессиональная ошибка. Если у 99 пользователей задержка 10 мс, а у одного — 10 секунд, средняя задержка покажет «комфортные» 110 мс, скрыв катастрофу 1% клиентов.
-
Метрики оцениваются по перцентилям (Percentiles):
-
p50(медиана): типичный опыт половины аудитории. -
p95 / p99: худший опыт 5% и 1% пользователей (хвост распределения / Tail Latency). Именно в p99 прячутся блокировки БД и спайки сборщика мусора.
-
-
Методология RED для микросервисов: Rate (частота запросов RPS), Errors (процент ошибок), Duration (длительность обработки по перцентилям).
-
Распределенная трассировка (Distributed Tracing / OpenTelemetry): Каждый входящий HTTP‑запрос на границе инфраструктуры получает сквозной идентификатор
TraceID(стандарт W3C TraceContext). Этот заголовок передается через все микросервисы, базы данных и очереди сообщений. Инженер в интерфейсе Jaeger или Grafana Tempo видит полный путь одного клика пользователя: сколько миллисекунд запрос провел в API‑шлюзе, сколько ждал ответа базы и на каком внешнем вызове споткнулся.
Инженерная матрица: вайбкодинг против системной разработки
|
Область |
Вайбкодинг (работа по наитию) |
Системная программная инженерия |
|---|---|---|
|
Вычисления и I/O |
Блокирует Event Loop тяжелыми вычислениями, плодит дескрипторы |
Понимает оверхед системных вызовов, применяет неблокирующий I/O ( |
|
Сложность алгоритмов |
Пишет вложенные циклы на |
Анализирует алгоритмическую сложность, использует Hash Map ( |
|
Типы и величины |
Считает деньги в числах с плавающей точкой (float/number), путает таймзоны |
Ведет расчеты строго в |
|
Базы данных |
Ловит Lost Update при параллельных запросах, забывает индексы |
Учитывает цену индексов (B‑Tree/GIN), уровни изоляции ACID, снимает гонки через FOR UPDATE или оптимистичные версии |
|
Сетевые интеграции |
Вызывает голый |
Ставит тайм‑ауты, использует Circuit Breaker, Exponential Backoff + Jitter и гарантирует идемпотентность через |
|
Распределенные данные |
Пишет в БД и в очередь сообщений подряд (Dual Write) |
Учитывает теорему CAP, проектирует надежную доставку через паттерн Transactional Outbox |
|
Безопасность |
Допускает IDOR/BOLA, склеивает SQL строками, ставит галлюцинированные пакеты |
Валидирует владение объектами, параметризует SQL, хеширует пароли через Argon2id, фиксирует хэши зависимостей в lockfile |
|
Архитектура |
Сваливает бизнес‑логику, запросы к БД и UI в один нечитаемый файл |
Разделяет слои через Dependency Inversion, визуализирует систему по модели C4, применяет принципы SOLID и фиксирует решения в ADR |
|
Тестирование |
Проверяет код руками в браузере или пишет 2 тривиальных unit‑теста |
Строит пирамиду тестов, запускает Testcontainers для интеграций, проверяет инварианты через Property‑Based Testing |
|
Наблюдаемость |
Ищет баги по |
Пишет структурированные JSON‑логи, строит трассировку OpenTelemetry, контролирует метрики по перцентилям p95/p99 |
Заключение: ИИ как экзоскелет, а не замена фундаменту
Генеративные модели и вайбкодинг не отменяют программную инженерию — они поднимают планку входа на уровень архитектора.
Когда рутинное написание синтаксического бойлерплейта дешевеет до нуля, ценность разработчика полностью смещается в область проектирования границ, формулирования инвариантов, ревью безопасности и понимания того, как система ведет себя под реальной нагрузкой и при неизбежных сбоях.
В руках человека без понимания базы нейросеть становится генератором технического долга со скоростью света. В руках системного инженера она превращается в мощнейший экзоскелет, позволяющий за дни проектировать и собирать надежные, отказоустойчивые production‑ready системы.
Автор: kimchi621


