Блог AST-SoftPro
System design юридической CRM: обращения, договоры, судебные и претензионные дела
Введение
Проектирование CRM для юридической компании — задача, которая на первый взгляд похожа на обычную бизнес-систему, а на практике несёт в себе ряд специфических требований: строгий аудит всех действий, работа с документами длительного хранения, сложные статусные модели дел и необходимость масштабирования на десятки филиалов. В этой статье разберём ключевые вопросы system design для юридической CRM с расчётом нагрузок на 30 филиалов и 100 сотрудников, а также предложим конкретный стек на Python.
Специфика юридической CRM: что отличает от обычной CRM
Обычная CRM хранит контакты, сделки и задачи. Юридическая CRM работает с принципиально другим набором сущностей и требованиями:
Обращения (intake). Клиент обращается с вопросом — по телефону, через сайт, лично в филиале. Обращение должно быть зарегистрировано за минуты, а не часы. Каждое обращение привязывается к конкретному филиалу и сотруднику. При этом обращения часто конвертируются в договоры или остаются «холодными» — система должна гибко отражать оба сценария.
Договоры. Договор — это не просто документ, а жизненный цикл: черновик → согласование → подписание → исполнение → закрытие/продление. Каждый переход между статусами фиксируется, прикладываются версии документов, отслеживаются обязательства сторон и сроки.
Судебные дела. Судебное дело имеет жёсткую привязку к процессуальным стадиям: подача иска, принятие к производству, предварительное заседание, судебные заседания, решение, апелляция, исполнение. Каждое событие (заседание, ходатайство, определение) фиксируется с датой и приложением документов.
Претензионные дела. Претензия — это досудебный этап: отправка претензии, срок ответа контрагента, результат (принята/отклонена/без ответа). Если претензия не увенчалась успехом, она конвертируется в судебное дело с переносом истории.
⚠️ Ключевое отличие: в юридической CRM каждое действие должно быть аудируемым. Кто изменил статус дела, когда, что было до и после — это не «лог действий», а юридически значимая информация, которая может потребоваться через годы.
Расчёт нагрузки: 30 филиалов, 100 сотрудников
Базовые допущения
| Параметр | Значение | Обоснование |
|---|---|---|
| Филиалов | 30 | По условию |
| Сотрудников (юристы + администраторы) | 100 | ~3–4 на филиал |
| Пиковая нагрузка (9:00–12:00) | 80% сотрудников онлайн | Утренний пик работы с документами |
| Среднее количество действий на сотрудника в час | 15–25 операций | Заполнение полей, поиск, переходы между экранами |
| Пиковый RPS (операций/сек) | ~30–40 | 80 * 20 / 3600 ≈ 0.44 опс на одного; с учётом API-вызовов (1 операция = 2–5 запросов к БД) → ~35 RPS на БД |
| Пиковый RPS на API | ~150–200 | Учёт нескольких запросов на одно действие пользователя |
Оценка объёма данных
| Сущность | За год (оценка) | Объём записи |
|---|---|---|
| Обращения | 3 000–6 000 | ~5 KB (текст, метаданные) |
| Договоры | 1 500–3 000 | ~2 KB + вложения (документы отдельно) |
| Судебные дела | 300–800 | ~3 KB + события (~5–15 на дело) |
| Претензионные дела | 400–900 | ~2 KB + события |
| Пользователи/филиалы | 100 / 30 | Минуемо |
| Итого за год | ~6 000–12 000 записей | ~50–150 MB без вложений |
Вложения (сканы, PDF) хранятся отдельно — в объектном хранилище или на файловом сервере. В БД хранятся только метаданные и пути.
Вывод по нагрузке
Для 30 филиалов и 100 сотрудников нагрузка не является высоконагруженной в классическом понимании (тысячи RPS). Это скорее среднезагруженная система с критичностью к:
-
Низкой латентности (< 200 мс на запрос) — юристы не ждут
-
Доступности (99.5%+ uptime) — простой CRM = простой работы всего офиса
-
Аудиту — каждое изменение фиксируется
-
Масштабируемости по филиалам — добавление 10 новых филиалов не должно требовать переписывания архитектуры
💡 Практический совет: при такой нагрузке модульный монолит с PostgreSQL и одним Redis более чем достаточен. Микросервисы на этом этапе избыточны и усложняют разработку без реальной выгоды по производительности.
Архитектурные решения
Модульный монолит vs микросервисы
Для описанного масштаба (100 пользователей, ~40 RPS на БД) модульный монолит — оптимальный выбор:
-
Один деплой, одна кодовая база, один цикл CI/CD
-
Модули (intake, contracts, litigation, claims) разделены по пакетам с чёткими границами
-
При росте до 100+ филиалов и 500+ сотрудников модуль «судебные дела» можно вынести в отдельный сервис без переписывания остальных
⚠️ Типичная ошибка: начинать с микросервисов «на будущее». Для 100 пользователей это означает 3–5 отдельных деплоев, межсервисную коммуникацию, распределённые транзакции — всё это усложняет поддержку без реальной нагрузки.
Модель данных: реляционная + event log
Юридическая CRM требует двух слоёв данных:
-
Текущее состояние — обычные таблицы PostgreSQL:
clients,intakes,contracts,cases,claims. Используются для быстрого чтения и поиска. -
Журнал событий — append-only таблица
eventsс колонками(id, entity_type, entity_id, event_type, actor_id, timestamp, payload JSONB). Каждый переход статуса, изменение поля, добавление документа фиксируется здесь.
Такой подход не является полным CQRS/event sourcing (это было бы избыточно для данного масштаба), но даёт:
-
Полную аудит-трейл без триггеров
-
Возможность восстановления состояния на любую точку времени
-
Гибкость при изменении бизнес-логики (старые события остаются валидными)
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(32) NOT NULL, -- 'intake', 'contract', 'case', 'claim'
entity_id BIGINT NOT NULL,
event_type VARCHAR(64) NOT NULL, -- 'created', 'status_changed', 'document_added'
actor_id INTEGER REFERENCES users(id),
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
payload JSONB NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_events_entity ON events (entity_type, entity_id, occurred_at DESC);
Статусные модели и workflow
Каждая сущность имеет конечный автомат статусов. Для судебного дела:
submitted → accepted → preliminary_hearing → hearing → decision
↓ ↓ ↓ ↓ ↓
rejected dismissed postponed postponed appealed
↓
enforcement / closed
Реализация: таблица case_statuses с колонками (status, allowed_transitions JSONB). Переходы валидируются на уровне сервиса — нельзя перейти из hearing в closed, минуя decision.
💡 Совет: храните допустимые переходы в БД, а не хардкодом. Это позволяет администраторам настраивать workflow без деплоя при изменении процессуального законодательства.
Мультифилиальность и права доступа
30 филиалов — это не мультиарендность (multi-tenancy) в классическом смысле, но требует разграничения данных:
-
Сотрудник видит дела своего филиала по умолчанию
-
Руководитель филиала видит все дела филиала
-
Штаб/адвокат может видеть дела нескольких или всех филиалов (настраивается через роли)
Реализация: колонка branch_id на основных сущностях + таблица user_branch_grants (user_id, branch_id, access_level). В запросах — фильтр по разрешённым филиалам. Для 30 филиалов это не создаёт проблем производительности; индексы по branch_id решают задачу.
Хранение документов
Документы (сканы исков, договоры в PDF, фото) не хранятся в БД:
-
Объектное хранилище (MinIO/S3-совместимое) или файловый сервер
-
В БД — таблица
documents (id, entity_type, entity_id, file_key, mime_type, size_bytes, uploaded_by, uploaded_at) -
Версионирование: при замене документа создаётся новая запись со ссылкой на предыдущую
Для 30 филиалов с оценкой ~5–10 GB вложений в год — MinIO на одном сервере более чем достаточно. При росте до 100+ филиалов переходим на S3-совместимое облако или кластер MinIO.
Рекомендуемый стек на Python
Сводная таблица
| Компонент | Выбор | Обоснование |
|---|---|---|
| Web-фреймворк | FastAPI | Async, автогенерация OpenAPI, Pydantic-валидация. Для ~200 RPS — с запасом в 10–20 раз |
| ORM / DB layer | SQLAlchemy 2.0 (async) + asyncpg | Зрелый ORM с поддержкой async; asyncpg — самый быстрый драйвер PostgreSQL для Python (примерно в 7 раз быстрее psycopg по bytea) |
| База данных | PostgreSQL 16 | JSONB для payload событий, полнотекстовый поиск (tsvector), частичные индексы. Для ~100 MB/год — один сервер достаточно |
| Кэш / сессии | Redis 7 | Сессии, кэш частых запросов (списки дел по филиалу), rate limiting. Hit rate цель: 80%+ |
| Очередь задач | Celery + Redis | Отправка уведомлений, генерация PDF, синхронизация с внешними системами. Celery — зрелый выбор с экосистемой (flower для мониторинга) |
| Файловое хранилище | MinIO (S3-совместимое) | Версионирование, доступ по presigned URL, масштабируемость |
| Аутентификация | JWT + refresh token | Stateless, подходит для SPA-фронтенда. Refresh — 7 дней, access — 15 минут |
| Логирование / аудит | structlog → JSON → файл/ELK | Структурированные логи; события из events таблицы дублируются в лог для мониторинга |
| Мониторинг | Prometheus + Grafana | Метрики: RPS, P95/P99 latency, размер очереди Celery, hit rate Redis |
| Фронтенд | React + TypeScript (или Vue 3) | SPA с роутингом по модулям. Для 100 пользователей — один фронтенд-бандл достаточно |
Почему FastAPI, а не Django?
Django — отличный выбор для CRUD-приложений с админкой «из коробки». Но для юридической CRM:
-
Нужен async (одновременные запросы к БД и файловому хранилищу)
-
Нужна точная контроль над сериализацией (Pydantic vs Django serializers — Pydantic быстрее и типобезопаснее)
-
Нужно OpenAPI-документирование для интеграций с внешними системами (судебные порталы, электронная подпись)
-
Django Ninja закрывает часть этих потребностей, но FastAPI + SQLAlchemy 2.0 async — более чистый стек без «Django-магии»
При этом Django остаётся валидным выбором, если команда сильнее в Django-экосистеме (admin, auth, forms).
Почему Celery, а не Dramatiq/RQ?
Для юридической CRM фоновые задачи:
-
Отправка email/SMS уведомлений о смене статуса
-
Генерация PDF-документов (договоры, претензии)
-
Периодическая синхронизация с внешними API (судебные реестры)
-
Расчёт SLA и напоминания о сроках
Celery даёт: приоритеты очередей, rate limiting на задачу, flower для мониторинга, celery beat для планирования. Dramatiq проще, но экосистема уже. RQ — слишком прост для 30 филиалов с разными очередями задач.
Ключевые вопросы производительности
Проблема N+1 и выборка данных
Типичный экран «Список судебных дел филиала» запрашивает:
-
Список дел (50 записей)
-
По каждому делу: последний статус, следующее заседание, ответственный юрист
Наивная реализация = 1 + 50*3 = 151 запрос. Оптимизация: SELECT с JOIN и подзапросами, либо joinedload в SQLAlchemy. Пример для SQLAlchemy 2.0:
stmt = (
select(Case)
.where(Case.branch_id == branch_id, Case.status != 'closed')
.options(joinedload(Case.current_status), joinedload(Case.assignee))
.order_by(Case.next_hearing_date)
.limit(50)
)
Один запрос вместо 151. Разница: ~2 мс vs ~300 мс на экране.
Индексация для юридической CRM
Критичные индексы:
-
cases (branch_id, status)— основной фильтр «дела моего филиала в работе» -
events (entity_type, entity_id, occurred_at DESC)— аудит по сущности -
intakes (branch_id, created_at DESC)— лента обращений -
contracts (party_name GIN trgm)— полнотекстовый поиск по контрагенту
Кэширование
| Что кэшируем | TTL | Почему |
|---|---|---|
| Список дел филиала (список, без деталей) | 30 сек | Часто открывают «ленту», данные меняются редко |
| Справочники (статусы, типы дел, роли) | 1 час | Меняются администратором раз в квартал |
| Результаты полнотекстового поиска | Не кэшируем | Слишком много уникальных запросов |
⚠️ Кэш не должен скрывать проблемы. Если P95 latency > 200 мс — сначала оптимизируйте запросы, а уже потом добавляйте кэш. Кэш как «костыль» для медленных запросов создаёт проблемы с консистентностью.
Безопасность и соответствие требованиям
Юридическая CRM хранит персональные данные клиентов (ФИО, паспортные данные, реквизиты). Требования:
-
Шифрование при передаче: TLS 1.3 на всех соединениях
-
Шифрование в покое: PostgreSQL —
pgcryptoдля чувствительных полей; MinIO — server-side encryption -
Маскирование в логах: ФИО, паспортные данные не пишутся в логи
-
Разграничение доступа: RBAC с минимальными привилегиями. Юрист видит только дела своего филиала (по умолчанию)
-
Аудит: таблица
events+ логи доступа (кто когда открыл дело). Хранение — минимум 5 лет (требование законодательства о персональных данных) -
Резервное копирование: PostgreSQL PITR (восстановление на определённую точку времени), MinIO — репликация. RPO ≤ 1 час, RTO ≤ 4 часа
⚠️ Для продакшн-решений с персональными данными рекомендуется привлечение специалистов по безопасности. Поверхностная реализация аутентификации или разграничения доступа может привести к утечке данных клиентов — это не только юридическая ответственность, но и потеря репутации.
Масштабирование: путь от 30 до 100+ филиалов
| Этап | Филиалы / Сотрудники | Архитектура | Что меняется |
|---|---|---|---|
| Старт | 30 / 100 | Монолит, 1 PostgreSQL, 1 Redis, 1 MinIO, 2–4 воркера FastAPI | — |
| Рост | 60–80 / 250 | + read replica PostgreSQL (для отчётов), + отдельный Celery worker для тяжёлых задач | Read replica разгружает основной сервер при генерации отчётов |
| Масштаб | 100+ / 500+ | Вынос модуля «судебные дела» в отдельный сервис (если нужно независимое масштабирование), шардирование по branch_id (PostgreSQL Citus или на уровне приложения) |
Шардирование — только если одна БД не справляется с P99 < 200 мс |
💡 Практический опыт: большинство юридических компаний останавливаются на этапе «Рост». Шардирование для CRM на 100 филиалов — редкость. Скорее всего, потребуются read replica и оптимизация запросов, но не горизонтальное шардирование БД.
Типичные ошибки при проектировании
| Ошибка | Последствие | Как избежать |
|---|---|---|
| Хранение документов в БД (BLOB) | Раздувание БД, медленные бэкапы, невозможность версионирования | Объектное хранилище + метаданные в БД |
| Отсутствие event log | Невозможно восстановить «что было до изменения» при спорной ситуации | Append-only таблица events с первого дня |
| Жёсткий хардкод статусов | Изменение процессуального законодательства = деплой | Таблица статусов + допустимые переходы в БД |
| Один воркер FastAPI на всё | При генерации PDF (5–10 сек) блокируются обычные запросы | Отдельная очередь Celery для тяжёлых задач; async I/O не блокирует event loop |
| Отсутствие rate limiting | Один «зависший» клиентский скрипт может перегрузить API | Redis-based rate limit: 60 req/min на пользователя |
| Мультиарендность через схему (schema per tenant) | Сложность миграций, проблемы с connection pool | Для 30 филиалов — одна схема + branch_id фильтр. Schema-per-tenant — только при 100+ независимых аренд |
Заключение
Юридическая CRM на 30 филиалов и 100 сотрудников — это среднезагруженная система с критичностью к аудиту, латентности и доступности, а не к пропускной способности. Модульный монолит на FastAPI + SQLAlchemy 2.0 async + PostgreSQL 16 + Redis + Celery + MinIO закрывает все требования с запасом в 5–10 раз по производительности.
Ключевые решения:
-
Журнал событий (append-only) для полного аудита — не CQRS, но достаточный аудит-трейл
-
Статусные автоматы в БД — гибкость при изменении законодательства
-
Документы вне БД — MinIO/S3 + метаданные
-
Read replica при росте до 60+ филиалов для отчётности
Важно понимать: выбор стека — лишь половина задачи. Вторая половина — корректная реализация с учётом edge-кейсов (конкурентные изменения статуса, частичные сбои сети между филиалами), безопасности (разграничение доступа, маскирование PII) и производительности (проблема N+1, индексация). Команды, которые доверяют критичные задачи опытным разработчикам с пониманием системного дизайна, экономят время и деньги на исправлении ошибок, которые могли быть предотвращены на этапе проектирования.