DDD‑lite для основателей, которые всё ещё кодят без полной переработки
DDD-lite для основателей делает бизнес‑правила читаемыми, когда вы одновременно проектируете, деплоите и отлаживаете продукт. Узнайте простую структуру и быстрые проверки.

Почему код основателя быстро становится грязным
Код основателя обычно пачкается по одной скучной причине: побеждает скорость. Когда один и тот же человек проектирует фичу, пишет её, деплоит и отвечает на поддержку, самое простое место добавить правило — там, где код уже открыт.
Небольшое изменение может начаться в обработчике маршрута. Вы добавляете проверку, вызываете хелпер, правите фоновую задачу, затем подправляете запрос, чтобы данные соответствовали новому поведению. Фича работает, но правило теперь живёт в трёх местах, которые не знают друг о друге.
Становится ещё хуже, когда правила продукта прячутся в проверках UI. Кнопка отключена в приложении, значит правило кажется покрытым. Но запланированная задача, админ-скрипт или вызов API минуют этот экран и делают то, чего UI никогда бы не позволил. Теперь продукт ведёт себя по-разному для пользователей и по-другому в бэкенде.
Поздние исправления создают ту самую проблему, которую основатели ненавидят больше всего: баг, который выглядит простым, но меняет поведение в двух местах. Вы поправили API, потому что поддержка нашла пограничный случай. Неделю спустя тот же баг возвращается, потому что воркер всё ещё использует старый хелпер. Ничто по отдельности не выглядит явно сломанным. Правило просто не имеет единого владельца.
Отладка в этот момент быстро тормозит. Вы не можете открыть один файл и ответить на простой вопрос: «кто может это сделать и когда?» Нужно проследить правило через обработчики, хелперы, методы модели, задания и сырой SQL. Каждая часть по‑отдельности имеет смысл. Вместе они превращают пятиминутный фикс в целый день.
Вот где помогает DDD-lite. Проблема редко в объёме кода сама по себе. Настоящая проблема в том, что бизнес-логика расползается в части приложения, которые должны только перемещать данные, рендерить экраны или запускать задачи. Доставка фич по‑прежнему кажется быстрой какое-то время. Изменение мнения — нет.
Что на практике значит DDD-lite
DDD-lite — это страховочные ограничители, а не грандиозная система. Вы сохраняете те части доменного моделирования, которые делают бизнес-правила читаемыми, тестируемыми и изменяемыми. Всё остальное остаётся простым.
На практике это значит ясно назвать несколько бизнес‑концепций и дать их правилам одно место. В SaaS‑приложении такими понятиями могут быть Trial, Subscription, SeatLimit, Invoice или AccessPolicy. Если концепция влияет на деньги, риск или то, как работа проходит через продукт, ей заслужено реальный код, а не разброс по маршрутам, UI‑обработчикам и запросам.
Вам не нужны тяжёлые диаграммы, шесть слоёв или дерево папок, которое выглядит внушительно и замедляет вас. Большинству продуктов, которые ведёт основатель, не нужны репозитории для всего, доменные события для каждого изменения или интерфейсы для будущей команды, которой ещё нет. Если один простой метод объясняет правило лучше всего — используйте один простой метод.
Простой тест здесь работает хорошо: можете ли вы указать одно место и сказать «этот файл решает правило»? Если нет, правило будет дрейфовать. Тогда ломается ценообразование, странно себя ведут права или поддержка вынуждена объяснять, почему два пользователя получили разные результаты.
Код фреймворка должен оставаться в слое доставки. Контроллер, API‑маршрут, задача или действие в React должны принимать ввод, вызывать правило и возвращать результат. Они не должны решать, кто платит, когда заканчивается доступ или применяется ли исключение.
Такое разделение быстро окупается. Бизнес‑правила остаются читаемыми, баги легче прослеживать, изменения фич затрагивают меньше файлов, а тесты могут фокусироваться на поведении, а не на проводке. Поэтому лёгкий domain‑driven design хорошо работает для основателей, которые всё ещё шипят собственный код. Вы не моделируете всю компанию. Вы защищаете те несколько правил, которые причиняют боль, когда их запутывают.
Если правило меняет доход, доступ, одобрения или соответствие, вынесите его из фреймворка и дайте хорошее имя. Оставьте всё остальное как есть.
Начните с слов, которые использует ваш продукт
Если клиент говорит «trial», «workspace» или «seat», код должен говорить то же самое. Код основателя становится грязным, когда язык продукта и язык кода расходятся. После нескольких быстрых фиксов «trial» превращается в SubscriptionStatus, AccessManager или plan_expires_at, и правило становится труднее читать, чем сам продукт.
Запишите существующие существительные и глаголы, которые уже встречаются в продажах, тикетах поддержки, заметках по онбордингу и тексте цены. Вам не нужен воркшоп для этого. Простой список достаточно, если он фиксирует слова, которые люди уже используют.
Затем закрепите каждое понятие за одним значением. Проблемы начинаются, когда «account» значит компанию в одном файле, логин в другом и запись биллинга где‑то ещё. Выберите одно значение и переименуйте остальные. Более длинные имена часто читаются лучше, потому что исключают догадки.
Несколько переименований обычно помогают сразу. TrialManager часто становится TrialPolicy или TrialExtension. UserService на самом деле может быть CustomerAccount или TeamMemberAccess. Utils почти всегда стоит переименовать во что‑то, что отражает реальную задачу.
То же правило применимо к бизнес‑логике. Пишите правила словами продукта, а не словами хранения. «Клиент может продлить триал один раз» — ясно. «Обновить строку subscription, если expires_at раньше X» — это шаг базы данных, а не бизнес‑правило.
Если приложение хранит даты триала в таблице subscriptions, верхний уровень кода всё равно должен читаться как trial.extendBy(7) или trial.canBeExtended(). Сохранение методов и имена колонок могут лежать ниже. Когда вы отлаживаете неудачное продление, вы хотите сначала увидеть правило, а SQL‑детали — вторым.
Это большая часть DDD‑lite. Выберите слова, которые уже использует продукт, держите их значения стабильными и выносите названия таблиц из основного пути. Когда файл читается как решение продукта, а не как схема базы данных, исправления багов происходят намного быстрее.
Проведите чёткие границы вокруг каждого правила
Когда один обработчик парсит ввод, проверяет бизнес‑правила, обновляет базу, отправляет письмо и вызывает платёжный API, баги становятся мутными. Вы читаете всю функцию, чтобы ответить на один вопрос: квалифицировался ли пользователь или побочный эффект упал после того, как правило уже прошло?
Чистое разделение начинается с ввода. Пусть контроллер, маршрут или воркер превращают грязные внешние данные в простые поля, которым ваш код может доверять. Затем передавайте эти поля в use case, который отвечает на один бизнес‑вопрос.
Use case должен звучать как решение, а не как транспортная задача. «Может ли этот клиент приостановить биллинг?» — ясно. «Обработать запрос на паузу подписки» обычно превращается в кладовку мусора.
Держите изменение состояния рядом с правилом, которое это позволяет. Если ваш продукт говорит, что команда может приглашать новых членов только пока её план активен, держите проверку и обновление счётчика участников в одном месте. Не проверяйте правило в одном файле и не обновляйте аккаунт в другом. Такое разделение выглядит аккуратно день‑два, а потом кусает.
Вы должны иметь возможность открыть одну небольшую область кода и увидеть четыре вещи без рытья: факты, которые читает правило, решение, которое оно принимает, данные, которые оно меняет, и результат, который возвращает.
Всё остальное — вне ядра. Доставка писем, очереди, аналитика и сторонние API важны, но это побочные эффекты. Выполняйте их после бизнес‑решения, а не внутри него.
Такая граница помогает больше, чем ожидают. Если правило говорит «approved», а почтовый провайдер таймаутится, вы всё равно знаете, что бизнес‑решение было верным. Можно повторить отправку письма, не перезапуская правило и не рискуя дублированием обновления.
Возвраты денег — хороший пример. Один use case должен решить, разрешён ли возврат, и пометить заказ соответствующим образом. Внешний слой может отправить письмо‑квитанцию и уведомить бухгалтерию. Когда поддержка спрашивает, почему возврат не прошёл, вы сначала смотрите на код правила, а не на мейлер, воркер очереди и клиент API одновременно.
Простая структура, которую можно настроить сегодня
Вам не нужен редизайн, чтобы получить читаемую бизнес‑логику. Начните с малого. Чистая настройка часто начинается с трёх папок: domain, use-cases и adapters.
Разделение простое. В domain лежат правила, которым должен следовать продукт. В use-cases — шаги для одного действия, например продление триала или возврат платежа. В adapters — внешние детали: база данных, почта, биллинг или очередь.
Это даёт каждой части одну задачу. Когда появляется баг, вы можете проверить правило, затем поток, затем внешний сервис. Вам не нужно копаться в одном огромном файле, где смешаны SQL, условные конструкции и API‑вызовы.
Выберите одно правило, которое уже доставляет боль. Не переносите пять сразу. Выберите что‑то маленькое, но реальное, вроде «триал можно продлить только один раз» или «счёт, помеченный как оплаченный, не может снова стать в черновик».
Поместите это правило в небольшой доменный объект или простую функцию. Держите её простой. Она не должна знать про HTTP, ORM‑модели или фоновые задания. Ей нужны только данные, необходимые для правила, и метод, который возвращает понятный результат.
Затем спрячьте вызов базы данных за тонким адаптером. Use case может загрузить запись триала, применить правило и сохранить результат. Доменный код остаётся читаемым, потому что ему всё равно, пришли данные из Postgres, Redis или тестового двойника.
Базовый поток достаточен: загрузить данные, вызвать правило, сохранить изменение. Перед тем как трогать следующую фичу, добавьте один тест для этого правила. Один тест уже даёт точку опоры при рефакторинге. Если правило говорит, что второе продление должно провалиться, напишите этот тест первым.
Этот подход лучше всего работает там, где код часто меняется. Оставляйте тихие части в покое. Каждый раз, когда вы трогаете грязную область, перемещайте одно правило, добавляйте один тест и продолжайте. Через несколько недель самые горячие части приложения обычно становятся гораздо спокойнее для отладки.
Реалистичный пример: продление триала
Звонок продаж заканчивается простым обещанием: этому аккаунту дадут ещё семь дней триала. Звучит мелко. В продуктах, которыми управляет основатель, это часто превращается в проверки биллинга, исключения планов, заметки поддержки и одного недовольного клиента через три недели.
Подход DDD‑lite держит это правило в одном месте. Контроллер не должен ничего решать. Он загружает аккаунт, передаёт его доменному объекту, спрашивает решение и реагирует на результат.
Представьте объект Account или TrialPolicy с методом вроде extendTrialByDays(7). Этот метод может проверить факты, которые важны для бизнеса: разрешает ли план ручные продления, предоставлял ли кто‑то уже продление и блокируют ли просроченные счета продление.
Затем объект возвращает понятный ответ. Да — продлить. Нет — отклонить. Он также должен возвращать причину простым языком, например "extension denied: overdue invoice" или "extension denied: already extended once". Это важно. Через шесть недель вы уже не вспомните, почему правило сработало, если код этого не скажет вслух.
Контроллер остаётся скучным. Он загружает аккаунт, спрашивает решение, сохраняет результат и только потом отправляет письмо. Если ответ — нет, он отправляет корректное сообщение. Если ответ — да, он обновляет дату окончания триала и сообщает клиенту новый дедлайн.
Такое разделение экономит время, когда поддержка получает жалобу. Ей не нужно смотреть контроллер, код биллинга и задачу отправки почты. Они смотрят в одно место: решение и его причину. Если причина неправильна — вы правите правило один раз. Если причина верна — поддержка отвечает клиенту без догадок.
Вот реальная выгода. Код совпадает с бизнес‑правилом настолько, что вы можете прочитать его во время ночной отладки и доверять тому, что он говорит. Вам не нужна большая доменная модель. Вам нужен один маленький объект, который отвечает за одно реальное решение.
Ошибки, которые утяжеляют DDD‑lite
DDD‑lite скатывается в проблему, когда структура появляется раньше правила. Если вы добавляете репозитории, сервисы, use cases, фабрики и политики до того, как сможете указать одно бизнес‑правило, которому нужна защита, вы построили церемонию, а не ясность.
Ещё распространённая ошибка — превращать каждую таблицу базы в объект с методами и пользовательским поведением. Большинство таблиц — просто сохранённые факты. Купон, пользователь или лог вебхука часто нормально работают как простые данные, пока настоящее правило живёт где‑то ещё.
Старая грязь также может вернуться под более красивым именем. Команды вытаскивают логику из контроллеров, а потом прячут её в хелперах или общих модулях с расплывчатыми именами. Через полгода никто не знает, является ли canRefund, checkRefund или refundPolicy тем правилом, которое реально важно.
Если решение влияет на деньги, доступ или обещания пользователям, поместите его в одно очевидное место и назовите бизнес‑термином. Это облегчает трассировку багов и сокращает угадывания, когда вы спешите что‑то исправить.
Дублирование — ещё один налог, который платят основатели позже. Правило добавляют в приложение, затем копируют в cron‑задачу, затем копируют ещё в обработчик вебхуков и админ‑панель. Это работает, пока одна копия не изменится, а другие нет.
Возьмём правило льготного периода для просроченных счётов. Оно не должно жить отдельно в коде синка биллинга, инструментах поддержки и дашборде клиента. Одна функция или маленький модуль должны отвечать на этот вопрос для всех этих мест.
Не гонитесь за чистотой. Вам не нужна сложная объектная модель для каждой фичи. Если простая функция вроде canExtendTrial(account, today) делает правило читаемым, тестируемым и легко отлаживаемым — хватит и её.
Хромая проверка запаха работает хорошо. Можете ли вы найти правило за 30 секунд? Может ли один и тот же код запускаться из API, админки и фоновой задачи? Если да — структура достаточно лёгкая. Если нет — уберите слои, прежде чем добавлять новые.
Быстрые проверки перед мерджем фичи
Перед мерджем отложите UI и посмотрите на само правило. Если правило кажется расползшимся, странно названным или трудным для тестирования, баг появится позже, когда вы будете усталы и торопитесь.
Начните с владения. Вы должны уметь указать один файл или один маленький модуль, который владеет решением. Если то же самое правило просачивается в контроллер, воркер и SQL‑запрос, вынесите его обратно перед мерджем.
Затем проверьте имена. Они должны совпадать со словами, которые ваша команда уже использует. Если поддержка говорит «продление триала», а в коде это promoWindow или bonusDays, люди будут говорить мимо друг друга.
Проверьте форму теста. Вы должны уметь тестировать решение без поднятия веб‑сервера. Дайте правилу простые входы, проверьте результат и двигайтесь дальше. Если тест требует HTTP, авторизации и базы данных — правило сидит не там.
Также держите логирование, парсинг запроса и SQL на краях. Сначала распарсьте запрос, затем вызовите правило, затем сохраните результат. Такой порядок сохраняет бизнес‑логику читаемой.
Наконец, представьте завтрашний баг‑репорт. Если кто‑то скажет «приостановленные аккаунты не должны получать дополнительные дни триала», вы измените одно место или три? Если ответ — три, не мержьте пока.
Простой пример делает это очевидным. Допустим, вы добавляете фичу, которая позволяет сейлзам продлить триал на семь дней. Правило должно жить в одном месте, которое отвечает на простой вопрос: может ли этот аккаунт получить продление и на сколько? Контроллер читает запрос, слой базы сохраняет новую дату. Никто из них не должен решать, кто подходит.
Это занимает лишние 15–20 минут в ветке фичи. Часто это экономит часы позже. Когда вы всё ещё проектируете, шипите и отлаживаете свой продукт, читаемая бизнес‑логика важнее умной архитектуры.
Куда двигаться дальше
Не рефакторьте всё приложение. Выберите один рабочий процесс, который постоянно отнимает у вас время, и почистите только этот путь.
Хорошие отправные точки обычно имеют ясное бизнес‑правило и реальную цену при его поломке. Продления триалов, смены планов, одобрения приглашений, запросы на возврат и лимиты мест — всё это подходит. Если поддержка постоянно задаёт один и тот же вопрос или вы снова и снова перечитываете один и тот же контроллер, начните с этого.
Маленький проход часто работает лучше, чем грандиозный редизайн. Выпишите термины продукта, которые люди путают. Вынесите бизнес‑правило в одно очевидное место. Добавьте пару тестов вокруг правил денег, доступа или одобрений. Затем задеплойте этот путь и посмотрите на следующие баг‑репорты. Если люди всё ещё путаются — подкорректируйте имена или границу.
Если хотите второе мнение перед тем, как трогать продакшн‑путь, oleg.is предлагает Fractional CTO и помощь по архитектуре стартапов, направленную именно на такого рода чистку. Полезная часть обычно не в добавлении паттернов, а в устранении путаницы вокруг правил, которые управляют доходом, доступом и работой поддержки.
Начните с одного правила, которое болит, а не с всего кодбазы. Если код после одного дня работы читается ближе к продуктовому решению, вы выбрали правильное место.
Часто задаваемые вопросы
Что такое DDD-lite на практике?
Думайте о DDD-lite как о простом правиле: каждой бизнес-решению — одно очевидное место. Ваши маршруты, UI, задания и SQL перемещают данные, а одна доменная функция или объект решает вопросы доступа, биллинга, возвратов или продления триалов.
Нужно ли переписывать приложение, чтобы это использовать?
Нет. Начните с одного болезненного пути и переместите только это правило. Небольшой рефактор вроде canExtendTrial(account, today) часто даёт пользу быстрее, чем масштабная чистка.
Какие правила стоит выносить в первую очередь?
Выберите правило, которое влияет на деньги, доступ, одобрения или соответствие требованиям. Эти правила дороже всего обходятся, когда рассеиваются, и обычно быстро окупают рефакторинг.
Как понять, что правило находится не в том месте?
Если для ответа на один продуктовый вопрос нужно открыть несколько файлов, правило уже расползлось. Ещё признак — когда UI блокирует действие, но API или фоновая задача всё равно его выполняют.
Что должен делать контроллер или маршрут?
Контроллер должен быть «скучным». Он парсит вход, вызывает правило, сохраняет результат и возвращает ответ. Он не должен решать, кто подходит, кто платит или кто получает доступ.
Как нужно называть правила и объекты?
Используйте те же слова, которые уже используются в продукте и поддержке. Если люди говорят trial, seat или workspace, поместите эти слова в код и дайте каждому термину одно значение.
Нужны ли репозитории, сервисы и дополнительные слои для каждой фичи?
Нет. Добавляйте структуру только там, где реальное правило нуждается в защите. Если одна простая функция делает решение читаемым и тестируемым, хватит и её.
Куда поместить отправку email, биллинг и вызовы очереди?
Побочные действия должны быть вне правила. Пусть доменная логика сначала принимает решение, сохраняет состояние, а уже затем отправляет письмо, вызывает биллинг или ставит задачу в очередь.
Как протестировать правило вроде продления триала?
Тестируйте решение без HTTP, аутентификации и базы данных посередине. Передайте правилу простые входные данные, проверьте результат и покройте ту грань, которая чаще всего ломается, например повторное продление триала.
Поможет ли это с исправлением багов и поддержкой?
Обычно помогает. Когда одно место владеет решением и возвращает причину вроде already extended once или overdue invoice, вы тратите меньше времени на догадки и больше — на исправление нужного места.