Временные бизнес‑правила в дизайне доменной модели
Узнайте, как моделировать временные бизнес‑правила для cutoff‑моментов, льготных периодов и запланированных действий, не пряча логику в утилитных функциях.

Почему временная логика становится запутанной
Временная логика редко возникает как сознательное проектное решение. Обычно она просачивается через быструю проверку: if an order arrives before 5:00 PM, approve it today. Потом та же идея появляется в фоновом задании, хелпере или контроллере. Со временем одно правило живёт в четырёх местах.
Именно это рассредоточение приносит проблемы. Один сервис проверяет, пришёл ли платёж до cutoff. Запланированная задача закрывает ожидания в полночь. Другой хелпер добавляет буфер в 10 минут, потому что служба поддержки попросила больше гибкости. Продукт теперь зависит от правил времени, но никто не может показать место, где они определены.
Мелкие исключения накапливаются быстро, потому что у времени много пограничных случаев. Пятница ведёт себя иначе, чем понедельник. Праздник сдвигает дедлайн. Одному клиенту дают льготный период, другому — нет. Повторная попытка, которая казалась безопасной в одной части системы, меняет представление другой части о просрочке.
Плата за это проявляется позже. Команда правит API, затем забывает воркер, который запускается каждый час. Биллинг говорит, что аккаунт всё ещё активен, но ночная задача уже его заблокировала. Поддержка читает одну политику, продукт следует другой, и пользователи получают смешанные сообщения. Такие баги отнимают время, потому что выглядят случайными, хотя реальная проблема — простое дублирование.
Команды часто считают время технической деталью, вроде форматирования метки времени или конвертации часового пояса. Это упускает суть. Если бизнес заботится о дедлайнах, буферах, датах продления или отложенных действиях, время должно быть частью домена.
Когда вы моделируете эти правила в домене, вы перестаёте рассыпать решения по утилитному коду. Код становится легче читать, изменять и тестировать. Ещё важнее — продукт начинает вести себя как единая система, а не как набор отдельных догадок о том, что означает стрелка часов.
Назовите правила до того, как писать код
Команды часто пишут проверки дат слишком рано. Пара строк if now > x кажутся безобидными, затем правило расползается по контроллерам, заданиям и хелперам. Через месяц никто уже не ответит на простой вопрос: что мы на самом деле обещали клиенту?
Пропишите правило в виде бизнес‑предложения до того, как коснётесь кода. «Заказы, размещённые до 17:00, отправляются сегодня.» «Клиенту даётся 15 минут на завершение оплаты.» «Мы повторяем неудачную попытку списания через 24 часа.» Если продакт‑менеджер, основатель или сотрудник поддержки могут прочитать это предложение и согласиться с ним, у вас есть достаточно ясное правило для моделирования.
Это также отделяет обещания клиенту от технической реализации. Обещание — это то, чего может ожидать клиент. Техническое расписание — это то, как система этого добивается. «Возвраты завершены в течение 3 рабочих дней» — это обещание. «Ворк будет запускаться каждый час» — это деталь реализации. Если смешивать их, небольшое изменение расписания задания может тихо изменить поведение продукта.
Давайте понятиям простые имена и используйте их последовательно: cutoff time, grace period, booking window, expiry time, retry delay. Скучные, но точные имена здесь работают лучше. canShipToday(orderTime) расскажет больше, чем isValidTime(). paymentExpiry понятнее, чем timeoutValue.
Держите каждое правило рядом с действием, которое оно контролирует. Если checkout отвечает за истечение срока оплаты, разместите правило в модели checkout или payment. Если за ежедневный cutoff отвечает доставка, храните его с логикой планирования отгрузок. Не прячьте временные решения в общих date‑хелперах только потому, что многие части приложения используют часы.
Простой тест работает хорошо: когда правило меняется, сможет ли один человек найти основное место для его правки за минуту? Если нет, правило всё ещё живёт разбросанно, а не в вашей модели.
Моделируйте cutoff‑времена как бизнес‑правила
Cutoff — это не просто отметка на часах. Это обещание о том, что бизнес сделает до этого момента и что изменится после. Если заказ, сделанный до 14:00, отправляется в тот же день, а сделанный после 14:00 — на следующий, такое правило принадлежит модели, отвечающей за доставку.
Голые метки времени не объясняют намерение. Поле вроде cutoff_at = 2026-05-10T14:00:00 говорит, когда что-то произошло, но не что этот момент значит. Лучше хранить смысл: cutoff для отправки в тот же день, дедлайн удержания платежа, срок подтверждения брони или крайний срок отмены.
Это небольшое изменение важно. Оно превращает правило времени в понятный объект, который можно читать, тестировать и менять без охоты по хелперам.
Для каждого cutoff определите обе стороны правила. До cutoff что разрешено? После cutoff что меняется? Иногда ответ прост: до 17:00 система принимает обработку в тот же день; после 17:00 задача ставится в очередь на следующий рабочий день. Иногда ответ строже: до дедлайна клиент может отменить онлайн; после него может вмешаться только поддержка.
Если вы не моделируете обе стороны, команды заполняют пробел самостоятельно. Один экран скажет «слишком поздно», другой всё ещё примет запрос, а воркер обработает его по‑своему. Ошибка не в математике дат — ошибка в том, что у продукта никогда не было одного понятного правила.
Добавляйте льготные периоды, не скрывая намерение
Льготный период должен решать конкретную проблему. Возможно, обработка карт занимает минуту, пользователь начал оформление прямо перед дедлайном, или пакетное задание завершается чуть позже. Если вы не можете объяснить, зачем нужен льготный период, он превратится в тихое правило, которое удивляет пользователей и путает команду.
Обращайтесь с ним как с отдельным правилом, а не как с секретным изменением основного дедлайна. Оригинальный cutoff всё ещё важен. Льготный период — это дополнительное разрешение с указанной причиной.
Также нужно чётко назвать начало и конец. Не говорите «примерно 15 минут после cutoff» и не оставляйте остальное утилитному коду. Скажите точно, когда он начинается и когда заканчивается. Например: счёт‑фактура должна быть оплачена до 17:00 по часовому поясу аккаунта, и льготный период идёт с 17:00:00 до 17:15:00.
Затем решите, что можно делать в этот интервал. Льготный период обычно позволяет завершить уже начатое действие, например повторить платёж или закончить оформление. Он может оставить аккаунт в режиме только для чтения или поддержать pending‑заказ, пока вмешается поддержка. Он не должен открывать дверь для совершенно новых действий, которые cutoff изначально блокировал.
Распространённая ошибка — считать льготное время молчаливым продлением. Это портит отчёты и превращает правила в домыслы. Если заказ опоздал, но принят в льготный период, модель должна сохранять оба факта: оригинальный cutoff и правило льготы, которое это разрешило.
Небольшое изменение формулировки показывает разницу. «Подписка продлевается до полуночи» — одно. «Подписка продлевается до полуночи, с 10‑минутным льготным периодом ТОЛЬКО для повторных попыток оплаты» — намного точнее. Пользователи получают шанс восстановиться, а команда может объяснить любой исход.
Планируйте запланированные действия шаг за шагом
Запланированная работа ломается, когда команды относятся к ней как к таймеру без контекста. Отложенное действие всё равно должно подчиняться тому же бизнес‑правилу в момент запуска, даже если прошло несколько часов или дней.
Хорошая модель отделяет решение от раннера заданий. Домен решает, что должно произойти позже, почему это должно произойти и когда это становится должным. Планировщик только «просыпается» и спрашивает: «Это ещё актуально?»
Простой поток выглядит так. Начните с бизнес‑события, например «счёт не оплачен до 17:00» или «завтра заканчивается триал». Позвольте этому событию создать в домене запланированное действие с явным временем исполнения и правилом, которое за ним стоит. Сохраните это время как данные, а не как догадку внутри фонового скрипта. Когда задача запускается, загрузите текущее состояние и снова спросите правило перед выполнением. Затем запишите результат: действие отправлено, пропущено, отменено или запланировано повторно, с указанием причины.
Повторная проверка важнее, чем кажется. Клиент может оплатить за две минуты до запуска напоминалки. Если задача доверяется старому решению, она отправит неверное письмо, и служба поддержки будет разгребать последствия.
Именно тогда код остаётся читаемым. Вместо того чтобы разбрасывать проверки дат по cron‑заданиям, воркерам и хелперам, держите правило рядом с объектом, который им владеет. Запланированная задача становится скучной — а это как раз то, чего вы хотите.
Небольшой пример проясняет идею. Допустим, аккаунт должен быть заблокирован через 24 часа после неудачной проверки соответствия. Домен может создать действие «заблокировать аккаунт» с due‑timestamp. Когда воркер подбирает его на следующий день, он снова проверяет аккаунт. Если пользователь исправил проблему, воркер помечает действие как отменённое и записывает причину.
И этот последний шаг важен. Когда кто‑то спрашивает: «Почему это действие выполнилось?» или «Почему оно не выполнилось?» система должна дать ответ без домыслов. Храните правило, время исполнения и результат в одном месте.
Решите вопросы часов, часовых поясов и календарей
Большинство багов времени появляются, когда разные части системы спорят о том, что такое «сейчас». Выберите один источник текущего времени и передавайте его в домен. Это может быть интерфейс Clock или небольшой сервис, но каждое правило должно читать время из одного места.
Это быстро окупается в тестах. Вы можете зафиксировать время на 16:59, за минуту до полуночи или в момент перехода на летнее время и проверить реальное поведение вместо догадок.
Локальное время должно существовать только там, где бизнес его требует. Храните метки времени в UTC по умолчанию, а затем конвертируйте их, когда правилу нужен локальный смысл. Cutoff для биллинга, рабочие часы офисов или окно доставки могут зависеть от города или страны. Задача очистки обычно не зависит от локального времени.
Запишите, какой часовой пояс владеет каждым правилом. «Заказы, размещённые до 17:00 Europe/Berlin, отправляются сегодня» — это ясно. «Заказы, размещённые до 17:00, отправляются сегодня» — нет. Размытая формулировка работает лишь случайно.
Небольшой пример показывает, почему это важно. Скажем, служба поддержки обещает ответы в тот же день для тикетов, открытых до 16:00 по Нью‑Йорку. Клиент в Токио отправляет тикет в 5:10 утра по местному времени. Правило всё равно должно проверять время Нью‑Йорка, потому что этот часовой пояс владеет обещанием.
Календари требуют того же подхода. Выходные, государственные праздники и рабочие дни не являются мелочью. Это часть правила. Если платёж переносится на следующий рабочий день, модель должна сказать, какой календарь принимает решение: банковские дни США, рабочие дни ОАЭ или ваш корпоративный календарь поддержки.
Делайте выбор явным. Один источник времени решает, что такое «сейчас» для домена. Каждое правило называет свой часовой пояс. UTC хранит метки времени, если не требуется локального смысла. Рабочие дни и праздники приходят из определённого календаря. Когда команды прячут эти решения в хелперах и расчётах дат, пограничные случаи тестировать становится намного сложнее.
Простой пример сценария
Приложение подписки продлевает платежи каждый месяц в 09:00 по часовому поясу плательщика. Это правило должно жить в доменной модели, а не в случайных date‑хелперах, потому что несколько частей продукта от него зависят.
Возьмём одного клиента на плане Pro. Он хочет перейти на Basic до следующего продления. Бизнес‑правило говорит: смена плана разрешена до 18:00 последнего дня текущего биллингового цикла. 31 мая в 17:50 приложение примет изменение и применит его к следующему счёту. В 18:01 оно не должно бросать расплывчатую ошибку. Должно быть записано, что текущий цикл продлевается на Pro, а более низкий план начнёт действовать после этого.
Этот cutoff — это не только правило UI. Биллинг, поддержка и фоновые задания должны давать одинаковый ответ на один вопрос: «Можно ли сейчас изменить план этой подписки?» Когда правило лежит в модели, все части системы остаются синхронизированы.
Далее списание за продление запускается 1 июня в 09:00 и терпит неудачу. Аккаунт не должен потерять доступ сразу. Бизнес даёт 72‑часовой льготный период, чтобы клиент успел исправить карту. В этот период подписка остаётся активной, но в другом состоянии. Модель может ясно сказать оба факта: доступ разрешён, и оплата просрочена.
Последующие действия исходят из тех же правил. В день продления в 09:00 система пытается списать. Если платёж не проходит, она устанавливает grace_period_ends_at на 4 июня в 09:00 и планирует повторные попытки на 2 и 3 июня в то же время. Если все повторы неудачны, аккаунт приостанавливается по окончании льготного периода.
Вот как выглядит хорошая временная логика. Одно место определяет cutoff, льготный период и расписание повторов. Остальная часть приложения запрашивает у модели даты и решения вместо того, чтобы снова собирать ту же логику.
Частые ошибки, вызывающие баги времени
Большинство багов времени начинается с простой путаницы: приложение читает серверное время, а бизнес‑правило использует другой источник. Биллинг может работать в UTC, а компания обещать «изменения в тот же день до 17:00 по Чикаго». Если код спрашивает «сейчас» у сервера и на этом останавливается, клиенты около cutoff получают неверный результат.
Ещё одна частая ошибка — прятать реальные правила в хелперах с именами вроде isValidTime() или canRunNow(). Эти имена звучат аккуратно, но скрывают причину решения. Был ли заказ всё ещё в пределах окна оплаты? Применялся ли складской буфер? Продлила ли поддержка дедлайн для одного аккаунта? Когда имя остаётся расплывчатым, люди переиспользуют метод там, где он не подходит.
Запланированные задания часто усугубляют ситуацию. Ночная задача может отменять неоплаченные заказы или освобождать резервации, но команды иногда позволяют задаче пропускать те же проверки, которые используются в обычных запросах. Задача смотрит на старую метку и действует. Доменное правило не выполняется. Получаются две правды: для живых действий и для фоновой работы.
Признаки обычно очевидны, если знать, что искать. Код сравнивает с локальным серверным временем. Хелпер скрывает бизнес‑решение за общим именем. Планировщик обновляет статус напрямую по меткам времени. Вы видите сырые значения вроде «15 минут» без метки, к какой политике они относятся. Каждая пауза или задержка называется льготным периодом, хотя по смыслу это может быть что‑то другое.
Магические числа стареют плохо. «15 минут» могут означать окно оплаты картой, буфер отправки или период охлаждения. Это разные правила. Дайте им имена, которые объясняют намерение: paymentGracePeriod или pickupCutoffBuffer.
Ещё одна ловушка — называть любую задержку льготным периодом. Некоторые задержки нужны для повторных попыток. Некоторые — для пакетной обработки. Некоторые защищают клиента от жёсткого cutoff. Если команда использует одну метку для всего, она начнёт применять неправильное поведение в неправильных местах. Чёткие имена, один бизнес‑источник времени и одинаковые проверки везде, где действие может произойти, предотвратят много боли.
Быстрая проверка перед релизом
Релиз не готов, если команда не может объяснить временные правила простым языком. Каждое правило должно укладываться в одно короткое предложение, например «Заказы, размещённые после 17:00, отправляются на следующий рабочий день» или «Клиенты могут отменить в течение 15 минут после оплаты». Если новому участнику нужно долго блуждать по хелперам, чтобы понять правило, оно спрятано слишком глубоко.
Также нужна одна очевидная зона ответственности для каждого правила. Cutoff должен жить в одном месте, а не частично в checkout, частично в фоне и частично в хелпере, который никто не хочет трогать. Этот единый источник делает правила проще менять без побочных эффектов.
Перед релизом проверьте несколько вещей:
- Протестируйте каждое правило сразу перед, точно в и сразу после границы.
- Убедитесь, что запланированные задания записывают, почему они выполнили, ждали или пропустили действие.
- Прочитайте пару логов и спросите, сможет ли сотрудник поддержки использовать их в реальном разговоре с клиентом.
- Выберите одно правило и попросите коллегу, который не создавал его, объяснить его вслух.
- Проследите каждое правило до одного объекта или сервиса, который владеет им.
Тесты граничных значений ловят те баги, с которыми люди реально сталкиваются. «16:59» обычно работает. «17:00» — место, где системы расходятся. «17:01» показывает, изменилось ли состояние так, как ожидал бизнес.
Логирование важно по той же причине. Когда запланированное действие запускается с опозданием, пропускает клиента или срабатывает дважды, команда нужна не только метка времени. Им нужна причина: вне рабочих часов, внутри льготного периода, ждёт подтверждения оплаты, выполнение заблокировано календарём праздников.
Служба поддержки первой ощущает качество этой работы. Если они могут объяснить клиенту, почему что‑то произошло, скорее всего модель понятна. Если им постоянно приходится просить инженеров расшифровать случаи, логика всё ещё утекает в разбросанный код.
Следующие шаги по очистке старой логики
Старые временные правила редко рушатся одновременно. Обычно они гниют в одном рабочем потоке вначале: утверждение закрывается слишком рано, продление запускается дважды или напоминание высылается после того, как пользователь уже действовал. Возьмите этот один запутанный путь и почистите его от начала до конца.
Узкая переработка работает лучше, чем масштабная чистка. Возьмите один поток, проследите каждое место, где код проверяет дату или время, и переместите эти проверки в именованные правила внутри доменной модели. Если метод называется isAfterCutoff(), canStillCancel() или scheduleRetryAt(), большинству продакт‑ и инженерных команд будет проще читать код без догадок.
Часто настоящая проблема — общие хелперы. Функция вроде addBusinessDays() или isExpired() кажется полезной, но скрывает намерение, когда каждая команда использует её по‑своему. Переименуйте хелперы вокруг бизнес‑правила, которое они поддерживают. «Оплата может быть подтверждена до 17:00 по местному времени» лучше, чем «сравнить метки времени и добавить смещение». Код может стать чуть длиннее в отдельных местах, но ему можно будет больше доверять.
Что менять в первую очередь
- Найдите один рабочий поток, где баги по времени уже отнимают у поддержки время или требуют ручных исправлений.
- Пропишите правило простым языком до того, как коснуться кода.
- Замените расплывчатые хелперы дат на имена, соответствующие поведению продукта.
- Добавьте тесты для граничных моментов: минута до cutoff, точный cutoff, конец льготного периода и выполнение отложенной задачи.
Эти тесты важнее большинства рефакторингов. Ошибки времени прячутся в пограничных случаях, а не в обычные дни. Если действие должно произойти завтра в 09:00, протестируйте 08:59, 09:00 и 09:01. Если льготный период длится 24 часа, проверьте точную передачу состояния, а не только «счастливый путь».
Если правила всё ещё кажутся мутными, рано привлекать продакт в переработку. Многие команды обнаруживают, что у них сначала не кодовая проблема, а неясная политика.
Иногда помогает короткий внешний обзор. Oleg Sotnikov на oleg.is работает со стартапами и малыми командами по архитектуре продукта, инфраструктуре и практическому внедрению ИИ; фокусированный ревью может помочь превратить разбросанную временную логику в небольшой набор ясных доменных правил без необходимости полного переписывания.