ТакПростоТакПросто.ai
ЦеныДля бизнесаОбразованиеДля инвесторов
ВойтиНачать

Продукт

ЦеныДля бизнесаДля инвесторов

Ресурсы

Связаться с намиПоддержкаОбразованиеБлог

Правовая информация

Политика конфиденциальностиУсловия использованияБезопасностьПолитика допустимого использованияСообщить о нарушении
ТакПросто.ai

© 2026 ТакПросто.ai. Все права защищены.

Главная›Блог›Data on the Outside vs Inside: уроки Пэта Хелланда для приложений
28 дек. 2025 г.·6 мин

Data on the Outside vs Inside: уроки Пэта Хелланда для приложений

Разбираем идею Data on the Outside vs Inside и почему приложениям нужны четкие границы, идемпотентность и сверка, когда сеть дает сбои.

Data on the Outside vs Inside: уроки Пэта Хелланда для приложений

Почему хорошие приложения ломаются из-за сети

Проблема не в том, что разработчики «плохо пишут код». Проблема в том, что сеть не дает гарантий. Запрос может идти дольше обычного, ответ может потеряться по дороге, а клиент может отправить тот же запрос еще раз, потому что «ничего не произошло». Для пользователя это выглядит как ошибка приложения, хотя на самом деле сеть просто ведет себя непредсказуемо.

Сеть обычно «врет» тремя способами: задерживает, теряет и дублирует. Задержка опасна тем, что вы не понимаете, выполнена операция или нет. Потеря приводит к частичному успеху: одна часть системы уже приняла действие, а другая о нем не узнала. Дубли создают повторное выполнение: один и тот же шаг сработал дважды.

Из этого и рождаются знакомые симптомы:

  • двойные списания или два одинаковых заказа
  • заказ «пропал» после оплаты, хотя деньги ушли
  • статус в приложении один, а в поддержке и бухгалтерии другой
  • «зависшие» операции, которые не завершаются сами

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

Цель проектирования простая: ошибки сети не должны ломать бизнес-результат. Пользователь может нажать «Оплатить» два раза, а система все равно должна прийти к правильному итогу: деньги списаны один раз, заказ создан один раз, статусы понятны, а расхождения можно найти и исправить. Для этого нужны четкие границы ответственности, идемпотентность и механизмы сверки. Практический смысл идеи Data on the Outside vs Inside как раз в этом.

Идея Хелланда: данные «снаружи» и «внутри»

Пэт Хелланд много лет разбирал сбои в реальных системах и свел их к простой мысли, полезной для продакшена. Его подход помогает перестать рассчитывать на «идеальную распределенную транзакцию» и начать проектировать так, чтобы приложение выживало при потерях сети.

Фраза Data on the Outside vs Inside делит мир на две зоны.

Снаружи (outside) живут сообщения, интеграции и ожидания. Здесь всегда есть неопределенность: запрос может дойти два раза, ответ может потеряться, а порядок событий может поменяться. Поэтому снаружи вы не «гарантируете», а договариваетесь: через контракты, идентификаторы, правила повторов и способы проверки результата.

Внутри (inside) находится то, что вы контролируете: локальная база данных, транзакции, инварианты. Здесь можно быть строгими: либо изменение записалось целиком, либо не записалось вовсе. Внутри вы держите порядок: уникальность, балансы, статусы, запреты на «невозможные» переходы.

Главная мысль: внутри можно гарантировать, снаружи нужно принимать хаос как норму и строить защиту.

Удобный прием - выписать границу системы и ответить на несколько вопросов:

  • Какие данные считаются «истиной» внутри?
  • Какие сообщения приходят извне и могут повторяться?
  • Как вы поймете, что операция уже выполнена?
  • Что будет, если подтверждение потерялось?

Пример: сервис заказов пометил заказ как «оплачен», но подтверждение платежа не дошло до витрины или склада. Внутри сервиса заказов статус уже истина, а снаружи другие системы еще «не в курсе». Это нормально, если у вас есть правила сверки и повторной доставки.

Четкие границы: где заканчивается ответственность

Пока вы не провели границу, у вас нет системы, есть только набор взаимных ожиданий. Граница - это место, где вы точно знаете, кто принимает решение, кто хранит правду и кто отвечает за последствия. Это может быть отдельный сервис, модуль в монолите или bounded context предметной области.

Внутри границы вы фиксируете «данные внутри»: их можно проверять, менять и защищать правилами. Обычно это значит:

  • инварианты (что всегда должно быть верно, например «оплата не может быть отрицательной»)
  • валидация входных команд
  • транзакции там, где они действительно нужны
  • единый источник истины для своих данных
  • понятные статусы (например «платеж создан», «платеж подтвержден»)

Снаружи границы начинается «данные снаружи»: сеть, очереди, другие сервисы и непредсказуемые задержки. Тут нельзя обещать мгновенную согласованность. Здесь нормальны асинхронность, повторные попытки, дубли и eventual consistency. Важно другое: внешний мир не должен обходить ваши правила напрямую.

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

Пример: сервис «Платежи» не должен напрямую менять «Склад». Он может только сообщить факт: «платеж подтвержден» или «платеж отклонен». А уже «Склад» сам решает, резервировать ли товар, списывать ли остатки и что делать при отмене. Тогда сбой сети превращается в задержку обработки, а не в тихую порчу данных.

Идемпотентность как защита от повторов

Идемпотентность в простых словах: вы можете повторить одно и то же действие несколько раз, а итог останется тем же, как если бы вы сделали его один раз. Нажали кнопку «Отправить» дважды, а письмо ушло только один раз. Это не про красоту кода, а про выживание приложения, когда сеть ведет себя плохо.

Повторы неизбежны. Клиент не получил ответ из-за таймаута, но сервер успел выполнить запрос. Очередь доставила событие повторно. Сервис упал после записи в базу, но до отправки подтверждения. В модели Data on the Outside vs Inside это нормально: «снаружи» нет надежной доставки, поэтому «внутри» нужно спокойно переживать повторы.

Практический прием - идемпотентный ключ. Это уникальный идентификатор операции (для создания заказа, списания, выдачи доступа), который вы принимаете с запросом и сохраняете внутри своей границы вместе с результатом.

Как это обычно работает: клиент отправляет операцию и idempotency_key. Сервис проверяет ключ. Если он уже был, возвращает тот же результат. Если ключ новый - выполняет действие, сохраняет ключ и итог, отвечает.

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

Для «почти идемпотентных» действий (деньги, бонусы) правило еще жестче. Делайте их как отдельные, явно пронумерованные операции: «списание N» или «начисление бонусов за заказ X». Тогда повтор превращается не во второе списание, а в повторный запрос того же списания.

Сообщения между системами: команды, события и порядок

Подготовьте продукт к запуску
Вынесите проект в прод с кастомным доменом и поддерживаемым хостингом.
Подключить домен

Как только данные вышли наружу, вы теряете полный контроль над тем, когда и в каком виде они вернутся. Поэтому важно различать команды и события.

Команда просит что-то сделать: «создай заказ», «спиши деньги». Событие сообщает о факте: «заказ создан», «платеж прошел». Команда может не выполниться, выполниться позже или выполниться дважды. Событие описывает то, что уже случилось, но может прийти с задержкой или повториться.

Порядок сообщений ненадежен. Даже если вы отправили «Платеж успешен» после «Заказ создан», другой сервис может увидеть их наоборот. Поэтому бизнес-логика не должна опираться на очередность доставки. Она должна опираться на состояние и правила: например, заказ можно перевести в «оплачен» только если он существует, а повторное событие об оплате не меняет итог.

Чтобы сообщения можно было безопасно обрабатывать, обычно хватает нескольких полей: идентификатор сообщения для дедупликации, тип и версия схемы, идентификатор сущности (например, order_id), корреляция (correlation_id или request_id) и время события или логическая версия (revision).

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

Сверка и согласование: как чинить расхождения

Расхождения неизбежны: сеть рвется, сервис не отвечает, сообщение приходит дважды или слишком поздно. Если «снаружи» у вас сообщения и чужие данные, то нужно уметь не только принимать их, но и регулярно проверять факты и поправлять то, что «поехало».

Сверка (reconciliation) - периодическая проверка: что система считает правдой сейчас и что действительно произошло по журналам, платежам, статусам у партнеров или в соседних сервисах. Дальше идет согласование: подтянуть недостающий статус, повторить безопасную операцию, закрыть зависшую попытку, исправить локальную запись.

Фоновые задачи нужны там, где нельзя гарантировать мгновенный ответ. Например: заказ создан, а подтверждение оплаты потерялось. Пользователь видит «в обработке», а фоновая задача через минуту спрашивает платежный сервис и доводит заказ до «оплачен». То же самое работает для доставки статусов, возвратов, выдачи доступа.

Чаще всего помогают два приема:

  • «Сага» как цепочка шагов: каждый шаг фиксирует результат и умеет компенсировать действие.
  • Outbox/inbox как дисциплина доставки: исходящие события сначала записываются рядом с вашими данными, входящие помечаются как уже обработанные.

Чтобы согласование не превратилось в «черный ящик», заранее задайте метрики: доля «зависших» операций, время до выравнивания, количество повторных доставок и обработок, число ручных вмешательств.

Рецепт проектирования под сбои сети

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

1) Начните с границы и инвариантов

Внутри вашей границы (одного сервиса или модуля с общей базой) выпишите инварианты: что нельзя нарушать ни при каких обстоятельствах. Например: «у заказа не может быть двух активных оплат», «баланс не уходит в минус», «статус меняется только вперед по цепочке».

Затем решите, какие данные окончательны внутри, а какие приходят «снаружи» и требуют проверки.

2) Опишите внешние взаимодействия как контракт

Для каждого внешнего вызова зафиксируйте: что вы отправляете (команда или событие), что ждете в ответ и что делаете, если ответа нет. Таймаут не означает «не сделано». Таймаут означает «не знаю».

3) Сделайте повторы безопасными

Почти все проблемы начинаются с повторов. Для каждого входящего запроса введите идемпотентный ключ и храните реестр обработанных запросов (например, таблицу processed_requests). Если пришло повторно, возвращайте тот же результат, не выполняя действие заново.

4) Настройте ретраи так, чтобы они помогали

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

5) Запланируйте сверку и наблюдаемость

Сверка должна быть регулярной: что сравниваем, как часто, где фиксируем итог (например, «расхождение найдено», «исправлено», «требует решения»). Добавьте корреляцию: один идентификатор на весь путь запроса, чтобы собирать историю в логах.

Пример: заказ, платеж и потерянное подтверждение

Заложите идемпотентность сразу
Соберите сервис на Go и PostgreSQL, где ключ идемпотентности хранится внутри границы.
Сгенерировать код

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

Сбой начинается в самом обычном месте: сеть. Платеж прошел, деньги списались, но callback или ответ API с подтверждением не дошел до сервиса заказов (таймаут, обрыв соединения, перегрузка очереди). Для магазина выглядит так, будто оплаты не было. Пользователь видит ошибку и нажимает «Оплатить еще раз».

Идемпотентность защищает от двойного списания. Магазин отправляет запрос на оплату не «просто так», а с ключом идемпотентности, например order_id + попытка_оплаты. Платежный сервис хранит этот ключ и действует по правилу: если запрос уже был обработан, вернуть тот же результат, а не создавать новый платеж.

Но даже с идемпотентностью заказ может зависнуть в неопределенности, если подтверждение потерялось. Тогда нужна сверка: периодическая проверка, которая находит платеж по стабильному ключу (например, payment_id или order_id) и приводит заказ в правильное состояние. Границы здесь критичны: платежный сервис отвечает за факт списания, сервис заказов - за статус заказа и выдачу товара.

Пользователю лучше показывать статусы, которые не обещают лишнего: «Ожидаем подтверждение оплаты», «Оплата принята, проверяем», «Оплачено, собираем заказ», «Оплата не подтверждена, попробуйте позже», «Нужна помощь поддержки».

Частые ошибки и ловушки

Распределенные системы ломаются не из-за «сложного алгоритма», а из-за мелочей вокруг сети: таймаутов, повторов запросов и частичных успехов. И именно тут идея Data on the Outside vs Inside помогает увидеть, где вы незаметно смешали «внутреннее» и «внешнее».

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

Типичные ошибки, которые потом дорого чинить:

  • склеивать операции разных сервисов в «один атомарный шаг» (заказ + платеж + склад), а при сбое не иметь ясного состояния, что уже сделано
  • воспринимать отсутствие ошибки как подтверждение успеха и игнорировать таймауты
  • делать действия неидемпотентными и получать дубли (два списания, два заказа, два письма)
  • не сохранять корреляцию: request_id, ключ идемпотентности, связь между командой и результатом
  • оставлять «вечные» статусы «в процессе» без механизма сверки или ручного решения

Если платежный сервис ответил клиенту «успех», а подтверждение в сервис заказов не дошло, поддержка быстро окажется в режиме гадания. Это почти всегда следствие того, что не продуманы статусы, сверка и правила восстановления.

Чеклист перед запуском

Проверьте сеть в реальном стенде
Поднимите фронт, бэкенд и базу и сразу проверьте таймауты и повторы.
Запустить

Перед релизом полезно проверить базовые вещи:

  • Для каждой внешней операции есть идемпотентный ключ: создание заказа, списание, выдача доступа, отправка письма.
  • Границы ответственности ясны: кто владеет данными о заказе, кто о платеже, кто о доставке, и где источник истины.
  • Повторы и нарушение порядка предусмотрены: дубликаты не ломают состояние, позднее событие не «откатывает» более свежий результат (если порядок важен, он явно проверяется по версии или номеру шага).
  • Есть план сверки: что сравниваем, как часто, что делаем при расхождении.
  • Пользователь не остается в тумане: статусы понятные, сценарий восстановления безопасный, повторная попытка не создает второй заказ.

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

Следующие шаги: как применить это в вашем проекте

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

Рабочая последовательность:

  • Описать границы: какие данные «внутри» каждого сервиса, а какие считаются «снаружи» и приходят сообщениями.
  • Выписать сообщения между частями (команды и события) и добавить к ним идемпотентность и корреляцию.
  • Смоделировать три сбоя: таймаут ответа, дубль запроса, потеря сообщения. Для каждого заранее решить, что увидит пользователь и что сделает система.
  • Ввести понятные статусы и правило, когда включаются повторы.
  • Запланировать сверку: как вы находите расхождения и как чините их без ручной паники.

Затем соберите маленький прототип и прогоните тестовые данные. Важно проверить три вещи: статусы не «врут», идемпотентность реально гасит повторы, а сверка находит и исправляет зависшие состояния.

Если вы делаете продукт в TakProsto, полезно сразу описать границы модулей и контракты сообщений, даже если приложение собирается быстро через чат. TakProsto (takprosto.ai) позволяет быстро собрать UI, сервер и базу, а затем безопасно проверять сценарии со сбоями с помощью снапшотов и отката, не боясь сломать рабочую версию.

FAQ

Почему приложение может «ломаться», даже если код написан нормально?

Потому что сеть не дает гарантий: запрос может задержаться, потеряться или дойти дважды. Для системы это выглядит как «непонятно, выполнилось или нет», и без защитных механизмов это превращается в дубли, пропажи статусов и зависшие операции.

Что значит «данные снаружи» и «данные внутри» по Хелланду?

«Внутри» — это зона, где вы контролируете данные и правила: ваша база, транзакции, инварианты, допустимые переходы статусов.

«Снаружи» — это вызовы по сети, очереди, интеграции и ответы, которым нельзя полностью доверять по времени, порядку и уникальности.

Как понять, где проходит граница системы и кто за что отвечает?

Сначала проведите границу ответственности: какой модуль/сервис принимает решение и хранит истину.

Практичный критерий: если другой компонент может обойти ваши проверки и записать состояние «напрямую», границы нет. Внешний мир должен только отправлять команды/события, а ваша часть — сама решать, как менять свои данные.

Что делать, если внешний запрос завершился таймаутом?

Таймаут означает «не знаю», а не «не сделано».

Правильная реакция:

  • не делать необдуманный повтор «в лоб»
  • проверять результат по стабильному идентификатору (заказ, платеж, операция)
  • показывать пользователю честный промежуточный статус и доводить операцию фоном или через сверку
Как работает идемпотентность и зачем нужен idempotency key?

Идемпотентность — это когда повтор одного и того же запроса не меняет итог (как будто он был выполнен один раз).

Обычно делается так: клиент передает idempotency_key, сервис хранит ключ и результат у себя. Если приходит повтор — возвращает уже сохраненный результат, не выполняя действие заново.

Где хранить ключ идемпотентности и реестр обработанных запросов?

Храните ключ там, где происходит эффект.

Примеры:

  • списание денег — в сервисе платежей
  • создание заказа — в сервисе заказов
  • выдача доступа — в сервисе доступа

Если хранить ключ «где-то снаружи», вы снова зависите от сети и получаете дубли при сбоях.

Чем отличается команда от события и почему это важно?

Команда — это просьба что-то сделать: «создай заказ», «спиши деньги». Она может прийти повторно и не обязана выполниться мгновенно.

Событие — это сообщение о факте: «заказ создан», «платеж подтвержден». Оно тоже может прийти поздно или повториться.

Полезное правило: не строить бизнес-логику на ожидании правильного порядка доставки; опираться на состояние и проверки.

Что делать, если события приходят не в том порядке?

Потому что сообщения могут прийти в другом порядке или с задержкой.

Чтобы не сломаться:

  • делайте обработку событий идемпотентной (дедупликация по message_id)
  • проверяйте допустимость перехода состояния (например, нельзя «откатить» статус более старым событием)
  • используйте версию/ревизию или явные правила приоритета свежих данных
Что такое сверка (reconciliation) и когда она нужна?

Сверка — это регулярная проверка фактов и устранение расхождений: найти «зависшие» операции, подтянуть статус, повторить безопасный шаг.

Обычно помогает:

  • фоновые задачи для проверки статусов (например, платежа)
  • outbox/inbox подход (фиксировать исходящие/входящие сообщения рядом с данными)
  • метрики: сколько зависло, как быстро выравнивается, сколько дублей
Как применить эти идеи, если я быстро собираю продукт на TakProsto?

Да, но правила те же: как только появляются несколько шагов и внешние сервисы, вы в распределенной системе.

Практика для проектов на TakProsto:

  • заранее описать границы модулей (кто владеет заказом, кто платежом)
  • ввести идемпотентные ключи для критичных операций
  • заложить понятные статусы для пользователя («проверяем оплату», «ожидаем подтверждение»)
  • использовать снапшоты и откат, чтобы безопасно проверять сценарии с таймаутами/повторами
Содержание
Почему хорошие приложения ломаются из-за сетиИдея Хелланда: данные «снаружи» и «внутри»Четкие границы: где заканчивается ответственностьИдемпотентность как защита от повторовСообщения между системами: команды, события и порядокСверка и согласование: как чинить расхожденияРецепт проектирования под сбои сетиПример: заказ, платеж и потерянное подтверждениеЧастые ошибки и ловушкиЧеклист перед запускомСледующие шаги: как применить это в вашем проектеFAQ
Поделиться
ТакПросто.ai
Создайте свое приложение с ТакПросто сегодня!

Лучший способ понять возможности ТакПросто — попробовать самому.

Начать бесплатноЗаказать демо