ADR-0056: Simplicity-first — целевая нагрузка, PostgreSQL как основная СУБД, закрытие сессий/просмотров polling-ом
Статус
Accepted (пункт про поиск/PostgreSQL-FTS — superseded ADR-0059; остальное в силе)
Дата
2026-07-13
Контекст
Целевая нагрузка PIN не является высокой:
| Метрика | Значение |
|---|---|
| Всего пользователей | до 1 000 000 (регистраций) |
| Агенты / менеджеры / менеджеры застройщиков | не более 20 000 |
| Остальные | инвесторы |
| Одновременно активных | 1 000 – 10 000 |
При такой нагрузке усложнение системы (микрооптимизации, сложные паттерны, дополнительные хранилища ради скорости) не окупается и вредит поддерживаемости.
Решение
1. Принцип simplicity-first (обязателен для всех детальных ТЗ)
- Не вводить сложные паттерны/архитектуру и микрооптимизации «на вырост» без измеренной необходимости.
- Простые CRUD-контексты (
user-activity/sessions,favorites,selectionsи подобные) проектируются максимально просто: PostgreSQL — основная и единственная СУБД, обычные таблицы + индексы,IDbRepositoryиз SDK. Redis для этих сервисов не используется (ни кэш, ни счётчики, ни сессии).Superseded (ADR-0059) — только про поиск. Ранее этот принцип трактовался как «поиск делаем на PostgreSQL-FTS (
tsvector/GIN), отдельное поисковое хранилище не добавляем». Это положение отменено: поиск во всех поверхностях PIN переведён на Elasticsearch-проекции черезTalent.Common.Domain.Search(ADR-0008 подтверждён). Не затрагивает остальное: PostgreSQL остаётся основной СУБД и source of truth; Redis по-прежнему не используется вsessions/favorites/selections. - Кэширование добавляется только точечно и по результатам измерений; на целевой нагрузке достаточно PostgreSQL + разумных индексов.
- Redis/backplane и прочие внешние компоненты — только там, где без них нельзя (см. п.4).
2. Отслеживание сессий и просмотров — требования
Несмотря на упрощение, обязательно отслеживаем:
- длительность сессии пользователя/гостя;
- длительность каждого просмотра объекта недвижимости;
- сколько раз и как долго просматривался каждый объект (агрегаты по лоту).
3. Механизм: LastActivityAt + polling-delta (без Redis/TTL-магии)
Записи Session и LotView (просмотр лота) ведутся в PostgreSQL:
- при старте:
CreatedAt= начало сессии / открытия лота;Status = Active; - активность (heartbeat/действия фронта) обновляет
LastActivityAtтой же записи (IDbRepository.Update(SetPropertyCalls)— дёшево, без загрузки агрегата); - фоновый worker (polling, background job SDK) периодически (например, раз в 1–5 минут) выбирает
записи
Status = Active, у которыхnow() - LastActivityAt > 10–15 минут(порог — конфигурация); - для каждой такой записи в одной транзакции:
Status → Inactive(завершена), вычисляется длительностьDuration = LastActivityAt - CreatedAt, публикуются Kafka-события через outbox:pin.user-activity.session-ended.v1(sessionId, userId/anonymousId, CreatedAt, LastActivityAt, Duration);pin.user-activity.lot-view-ended.v1(lotId, sessionId, userId/anonymousId, Duration, контекст фильтров);
- worker идемпотентен (повторный прогон не трогает
Inactive), выборка —FiltercSelectLockMode.ForUpdateSkipLocked; - агрегаты по лоту (
сколько раз,суммарно/в среднем как долго) — консюмерlot-view-endedинкрементально обновляет счётчики (LotViewStats: ViewsCount, TotalDuration, LastViewedAt) в PostgreSQL.
4. Границы применения других хранилищ (без изменения ранее принятого)
- ClickHouse — только там, где обосновано аналитикой: история цен в
invest-analytics(append-only, требование ТЗ) и view-статистика застройщиков вdev-stats(3 года facts + rollup-ы, ADR-0057). Операционные событияuser-activity— PostgreSQL (уточнение ADR-0054); новые применения CH — только через ADR. - Redis не используется в sessions/favorites/selections. Для SignalR-hub мессенджера на целевой нагрузке (1–10k активных) достаточно одного инстанса без Redis backplane; backplane — опция при реальном горизонтальном масштабировании (уточнение ADR-0055).
- Распределённые блокировки, если нужны, — Postgres-провайдер SDK (
DistributedLock.Postgres), не Redis. - Elasticsearch (полнотекстовый/fuzzy-поиск): simplicity-first не отменяет search-projection —
поиск во всех поверхностях PIN использует Elasticsearch-проекции через
Talent.Common.Domain.Search(ADR-0059, подтверждает ADR-0008). Source of truth остаётся в PostgreSQL сервиса; проекция строится по событиям + rebuild-джоб.
Последствия
- Детальные ТЗ контекстов
user-activity,favorites,selectionsпроектируются как простые PostgreSQL-CRUD с фоновым polling-worker-ом; никаких Redis/TTL/стриминговых пайплайнов. - Все ТЗ обязаны проходить проверку «не переусложнено ли» (verify-пас конвейера).
- При будущем росте нагрузки узкие места решаются точечно (индексы → реплики чтения → кэш) с измерениями.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (20): Классификация зрителя и источника (ViewerKind / ViewSource), Модели ответа dev-stats (типизированные ряды / точки / сводки), DevStatsFilterModel (общий вход метрик dev-stats), Entity (Replica): MessengerUserEntity, Entity (Replica): UserDisplayReplicaEntity, Entity / Aggregate: AppointmentEntity, Entity / Aggregate: ArbitrageCaseEntity (+ ArbitrageNoteEntity), Entity / Aggregate: AvailabilitySlotEntity, Entity / Aggregate: CalendarEventEntity, Entity / Aggregate: ChatEntity (+ParticipantEntity как owned-child), Entity / Aggregate: ChatEventEntity, Entity / Aggregate: KnowledgeBaseArticleEntity (+8)
- API (93): Admin: модерация постов и комментариев (m2m), GET /api/agent/crm/clients — Клиентская база агента (список + поиск), GET /api/agent/crm/leads/{leadId}/events — Лента событий лида, GET /api/internal/user-activity/users/{userId}/property-object-activity — Активность инвестора по объектам недвижимости (карточка лида CRM), GET /api/internal/user-activity/users/{userId}/property-objects/view-stats — Батч просмотров инвестора по объектам недвижимости (обогащение карточек CRM), GET /api/investor/crm/deals/{dealId}/timeline — Таймлайн сделки (инвестор), GET /api/messenger/calls — Журнал звонков (входящие / исходящие / пропущенные), GET /api/messenger/chats — Список чатов пользователя, GET /api/messenger/chats/{chatId}/conferences — История конференций сделки, GET /api/messenger/chats/{chatId}/messages — История чата (keyset-пагинация), GET /api/messenger/contacts — Список подтверждённых контактов, GET /api/messenger/contacts/search — Глобальный поиск пользователя по телефону (+81)
- Use Cases (30): UC-BKG-001. Инвестор инициирует бронирование с витрины, UC-BKG-002. Бронирование Резерв → Бронь, UC-BKG-003. Истечение резерва/брони и продление, UC-BKG-004. Оплата платного бронирования (чек + подтверждение), UC-CAL-008. Напоминания о событиях календаря, UC-CAL-011. Переназначение открытых дедлайнов и встреч при смене исполнителя, UC-CRM-002. Агент создаёт сделку из лида (выбор объекта, гейт вовлечения застройщика), UC-CRM-003. Движение сделки по канбан-доске, UC-CRM-004. Документы сделки: запрос → загрузка → проверка → статусы, UC-CRM-005. Оформление сделки: договоры → эскроу → Росреестр → завершение, UC-FIX-001. Агент отправляет заявку на фиксацию, UC-FIX-002. Застройщик одобряет фиксацию (авторизация работы с клиентом, без создания сделки) (+18)
- События (28): События сервиса messenger: реестр топиков, События сервиса support: карта топиков и решения об интеграциях, События dev-stats, Event Contract: pin.calendar.appointment-cancelled (AppointmentCancelledEvent), Event Contract: pin.calendar.appointment-completed (AppointmentCompletedEvent), Event Contract: pin.calendar.appointment-confirmed (AppointmentConfirmedEvent), Event Contract: pin.calendar.appointment-created (AppointmentCreatedEvent), Event Contract: pin.calendar.event-created (CalendarEventCreatedEvent), Event Contract: pin.calendar.event-owner-changed (CalendarEventOwnerChangedEvent), Event Contract: pin.calendar.event-rescheduled (CalendarEventRescheduledEvent), Event Contract: pin.calendar.event-status-changed (CalendarEventStatusChangedEvent), Event Contract: pin.calendar.reminder-due (ReminderDueEvent) (+16)
- Консюмеры (29): Consumer: Booking Expired Card (MessengerBookingExpiredConsumer), Consumer: Calendar CRM Booking Sync (calendar-crm-booking-events), Consumer: Calendar CRM Deal Sync (calendar-crm-deal-events), Consumer: Calendar Events → Notifications (notifications-calendar-events), Consumer: Calendar Identity User Sync (calendar-identity-users), Consumer: CRM Agent Offboarding (CrmAgentOffboardingConsumer), Consumer: CRM Calendar Appointments Sync (crm-calendar-appointments), Consumer: CRM Events → Notifications (notifications-crm-events), Consumer: CRM Identity User Sync (CrmUserUpsertedConsumer), Consumer: Deal Chat Provisioning (MessengerDealCreatedConsumer), Consumer: Deal Stage Card (MessengerDealStageChangedConsumer), Consumer: Favorites Events (UserActivityFavoriteAddedConsumer / ...RemovedConsumer) (+17)
- Фоновые задачи (29): Background Job: AccessFanOutJob — веерная рассылка UserAccessChangedKafkaEvent при смене состава прав роли, Background Job: AppointmentTimeoutJob, Background Job: BookingExpirationJob, Background Job: CallTimeoutReaperJob, Background Job: ChatProvisionRetryJob, Background Job: CommentCountReconcileJob, Background Job: ConferenceRoomReaperJob, Background Job: DelistedRetentionJob, Background Job: DimensionBackfillJob, Background Job: DocumentExpiryReminderJob, Background Job: ExpirePromotionsJob, Background Job: ExpireSessionsJob (+17)
- Саги (2): Saga «Booking» (Резерв → Бронь → передача в продажу), Saga «Deal Completion» (fan-out завершения сделки)
- Поиск (5): ClientSearchDocument, DealSearchDocument, FixationRequestSearchDocument, KbArticleSearchDocument, TicketSearchDocument
- Отчёты (1): Отчет/Export: dev-stats-export (выгрузка аналитики застройщика)
- Дашборды (4): Панель Метрик Застройщика: dev-stats-overview (аналитика внимания), Панель Наблюдаемости: news-overview (эксплуатация), Панель Наблюдаемости: support-overview (эксплуатация), Ops-борд: dev-stats-ingest-overview (здоровье ingestion)
- adr (9): ADR-0060: Продвижение — отдельный bounded context promotion (полиморфный таргет); catalog и news — консюмеры буста, ADR-0062: Медиа-стек звонков и запись на Jitsi (JVB SFU + coturn + jibri → file storage; JWT из messenger; нативный lib-jitsi-meet), ADR-0063 — справочники pin.handbook (репликация из Talent Handbook), ADR-0064 — контекст calendar (календарь и запись на показ/консультацию), ADR-0065 — контекст wallet (внутренние балансы и ledger), ADR-0066 — удержание ПИН и реферальные ставки, ADR-0067 — гейт получения денег (акт поступления средств), ADR-0068 — Scoped и time-bounded конфигурация комиссионных и реферальных ставок, ADR-0069: Co-broking — расщепление комиссии между несколькими агентами (дизайн, v-next)
- scenarios (5): UC-OVR-001. Запрос консультации у агента недвижимости, UC-OVR-003. Бронирование объекта у агента (+3a), UC-OVR-004. От реферальной ссылки до объекта у собственника, UC-OVR-010. От завершённой сделки до выплаты комиссии и реферального вознаграждения, UC-OVR-011. Самозапись инвестора: от слота до исхода встречи и лида в воронке
- index.md (10): Calendar Service, CRM Service, DevStats Service (Pin.DevStats), Handbook Service, Messenger Service, News Service (Pin.News), Notifications Service, Support Service, user-activity Service, Wallet Service
- calls (7): Жизненный цикл сессии звонка — 1:1 и конференция (messenger), Медиа-звонки и запись на Jitsi — интеграционный гид (messenger), Медиа-звонки на Jitsi: план поставки (playbook аналитик → разработчик), Backend-реализация медиа-звонков: спека → код (messenger), Jitsi-звонки: план верификации и приёмки, Jitsi: k8s-топология и взаимодействие с бэкендом, Jitsi: k8s/Helm-манифесты (SKETCH)
Связанные документы
- ADR-0054 (user-activity — уточнён: PostgreSQL-only), ADR-0055 (messenger — backplane опционален),
ADR-0059 (поиск на Elasticsearch-проекциях — supersedes FTS-пункт),
docs/06-services/user-activity/,docs/06-services/favorites/,docs/06-services/selections/.