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

Сегодня разберём, как это работает на примере Битрикса.

Что такое Strangler Fig

Мартин Фаулер подсмотрел идею у фикуса-душителя — растения, которое обвивает дерево-хозяина и постепенно его замещает. В архитектуре это означает: не выбрасываем монолит, а строим новую систему вокруг него, откусывая функционал кусочками.

Три этапа:

  • Transform — выносим часть логики в микросервис

  • Coexist — старое и новое работают параллельно

  • Eliminate — убираем дублирующийся код из монолита

Главное преимущество — возможность отката на любом этапе. Что-то по��ло не так? Возвращаем трафик в монолит, разбираемся спокойно.

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

Прокси-слой: точка принятия решений

Первое, что нужно — прослойка между клиентами и бэкендом. Она решает, куда направить запрос: в монолит или микросервис.

Простейший вариант на nginx:

map $request_uri $backend {
    default                     bitrix_monolith;
    ~^/api/v2/orders           order_service;
    ~^/api/v2/inventory        inventory_service;
    ~^/api/v2/notifications    notification_service;
}

server {
    location / {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Добавление нового сервиса — одна строчка в конфиге. Откат — удаление этой строчки. Никакого деплоя кода, изменения применяются за секунды через nginx -s reload.

Можно использовать и другие решения: Envoy, Traefik, Kong — принцип тот же. Выбор зависит от того, что уже используется в инфраструктуре.

Битриксовые вебхуки (/rest/{user_id}/{token}/...) на первых этапах лучше оставить в монолите. Трогать их имеет смысл, когда основная миграция завершена.

Входящие вебхуки: вызываем Битрикс из микросервисов

Битрикс24 предоставляет REST API через входящие вебхуки. Создаем вебхук в админке выбираем нужные права, получаем URL вида https://your.bitrix24.ru/rest/1/abc123xyz/.

Дальше дергаем методы, добавляя их к URL: .../crm.deal.list, .../crm.contact.add и так далее.

Пару нюансов:

Rate limiting. Битрикс ограничивает частоту запросов. На бесплатных тарифах около 2 запросов в секунду, на платных лимиты выше, но всё равно есть. Без троттлинга быстро упрётесь в ошибку QUERY_LIMIT_EXCEEDED.

class BitrixClient:
    def __init__(self, webhook_url: str):
        self.base_url = webhook_url.rstrip("/")
        self._last_request = 0.0
    
    def _throttle(self):
        elapsed = time.monotonic() - self._last_request
        if elapsed < 0.5:
            time.sleep(0.5 - elapsed)
        self._last_request = time.monotonic()

Retry с экспоненциальным backoff. Даже с троттлингом Битрикс иногда возвращает ошибки при пиковой нагрузке. Повторяем запрос с нарастающей задержкой:

def call(self, method: str, params: dict = None):
    url = f"{self.base_url}/{method}"
    
    for attempt in range(3):
        self._throttle()
        response = httpx.post(url, json=params or {}, timeout=30)
        data = response.json()
        
        if data.get("error") == "QUERY_LIMIT_EXCEEDED":
            time.sleep(2 ** attempt)  # 1s, 2s, 4s
            continue
        
        if "error" in data:
            raise BitrixAPIError(data["error"], data.get("error_description"))
        
        return data.get("result")
    
    raise BitrixAPIError("RETRY_EXHAUSTED", "Max retries exceeded")

Батчинг. Битрикс позволяет отправлять до 50 команд за один HTTP-запрос через метод batch. Обновление 200 сделок займёт 4 запроса вместо 200.

def batch(self, commands: dict) -> dict:
    """
    commands = {
        "deal1": ("crm.deal.get", {"ID": 1}),
        "deal2": ("crm.deal.get", {"ID": 2}),
    }
    """
    cmd = {alias: f"{method}?{urlencode(params)}" 
           for alias, (method, params) in commands.items()}
    
    result = self.call("batch", {"halt": 0, "cmd": cmd})
    return result.get("result", {})

Параметр halt: 0 означает продолжать выполнение даже при ошибке в одной из команд. Если нужно остановиться при первой ошибке — ставим halt: 1.

Исходящие вебхуку

Когда в Битриксе происходит событие (создали сделку, изменили статус, добавили комментарий), он может отправить POST-запрос на указанный URL. Настраивается в админке: выбираешь событие, указываешь endpoint.

Это позволяет микросервисам реагировать на изменения в реальном времени, не опрашивая Битрикс постоянно.

Пвроблемы, которые нужно решить:

Дубликаты. Битрикс иногда отправляет одно событие дважды — при сетевых проблемах, таймаутах, или просто потому что так получилось. Без дедупликации получите двойные записи.

Решение — хранить обработанные события в Redis с TTL:

@app.post("/webhooks/bitrix/deal")
async def handle_deal_webhook(request: Request):
    body = await request.body()
    payload = parse_webhook(body)
    
    # Дедупликация
    event_key = f"bitrix:event:{payload.event}:{payload.data['ID']}"
    if await redis.exists(event_key):
        return {"status": "duplicate", "message": "Already processed"}
    
    await redis.setex(event_key, 3600, "1")  # TTL 1 час
    
    # Обработка по типу события
    if payload.event == "ONCRMDEALADD":
        await handle_deal_created(payload.data)
    elif payload.event == "ONCRMDEALUPDATE":
        await handle_deal_updated(payload.data)
    
    return {"status": "ok"}

Потери. Если сервис недоступен в момент события — оно потеряно навсегда. Битрикс не повторяет отправку, не ведёт очередь.

Решение — периодическая сверка критичных сущностей. Раз в час (или чаще для важных данных) сравниваем состояние в Битриксе и микросервисе:

async def sync_deals():
    last_sync = await redis.get("deals:last_sync") or "2020-01-01"
    
    deals = bitrix.call("crm.deal.list", {
        "filter": {">DATE_MODIFY": last_sync},
        "select": ["ID", "STAGE_ID", "OPPORTUNITY"]
    })
    
    for deal in deals:
        await process_deal_update(deal)
    
    await redis.set("deals:last_sync", datetime.now().isoformat())

Безопасность. Любой, кто узнает URL вебхука, может отправить фейковое событие. Битрикс подписывает запросы — обязательно проверяйте подпись:

def verify_bitrix_signature(request: Request, body: bytes, secret: str) -> bool:
    signature = request.headers.get("X-Bitrix-Signature", "")
    expected = hmac.new(
        secret.encode(), 
        body, 
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Дополнительно можно ограничить IP-адреса, с которых принимаются вебхуки — Битрикс публикует список своих серверов.

Anti-Corruption Layer: изоляция моделей

ACL — прослойка, которая переводит данные между моделями монолита и микросервисов. Без неё код быстро превращается в такую вот кашу из битриксовых полей, разбросанных по всему проекту.

Пример проблемы: в Битриксе статус сделки — строка "PREPARATION", сумма — строка "15000.00", дата — в формате "DD.MM.YYYY". В микросервисе хочется нормальный enum, Decimal и datetime.

Адаптер инкапсулирует все преобразования:

class BitrixDealAdapter:
    STATUS_MAP = {
        "NEW": OrderStatus.NEW,
        "PREPARATION": OrderStatus.PROCESSING,
        "PREPAYMENT_INVOICE": OrderStatus.AWAITING_PAYMENT,
        "EXECUTING": OrderStatus.IN_DELIVERY,
        "WON": OrderStatus.DELIVERED,
        "LOSE": OrderStatus.CANCELLED,
    }
    
    @classmethod
    def to_order(cls, deal: dict) -> Order:
        return Order(
            id=f"BX-{deal['ID']}",
            external_id=deal["ID"],
            status=cls.STATUS_MAP.get(deal["STAGE_ID"], OrderStatus.UNKNOWN),
            total=Decimal(deal.get("OPPORTUNITY", "0")),
            created_at=cls._parse_date(deal.get("DATE_CREATE")),
            customer_id=deal.get("CONTACT_ID"),
        )
    
    @classmethod
    def from_order(cls, order: Order) -> dict:
        reverse_map = {v: k for k, v in cls.STATUS_MAP.items()}
        return {
            "STAGE_ID": reverse_map.get(order.status, "NEW"),
            "OPPORTUNITY": str(order.total),
        }
    
    @staticmethod
    def _parse_date(date_str: str) -> datetime | None:
        if not date_str:
            return None
        return datetime.strptime(date_str, "%d.%m.%Y %H:%M:%S")

Вся логика преобразования в одном месте.

Бонусом адаптер легко покрывается юнит-тестами отдельно от интеграционных тестов с реальным Битриксом.

Стратегия миграции

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

Рекомендуемый порядок:

  1. Аналитика и отчёты. Только чтение данных через batch-запросы. Падение сервиса аналитики не влияет на основной бизнес — просто отчёты показывают данные часовой давности.

  2. Уведомления. Email, SMS, push-уведомления. Вебхуки Битрикса триггерят сервис нотификаций. Потеря уведомления неприятна, но на самом деле не так критично,заказ-то создан и обрабатывается.

  3. Интеграции с внешними системами. Доставка (СДЭК, Boxberry), платёжные системы, склады. Битрикс сообщает о событии, микросервис взаимодействует с внешним API, результат пишется обратно в Битрикс.

  4. Каталог товаров. Синхронизация, кеширование, поиск. Здесь уж�� двусторонний обмен данными и вопросы консистентности.

  5. Заказы и CRM. Ядро. Браться только когда набита рука на предыдущих этапах и есть уверенность в инфраструктуре.

Каждый этап — отдельный деплой с возможностью отката. Между этапами могут проходить недели или месяцы.

Пару проблем

Два источника правды. Данные есть и в Битриксе, и в микросервисе. Какие актуальные?

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

Таймауты и circuit breaker. Любое решение, вне зависимости от архитектуры, может медленно отвечать при сложных запросах или под нагрузкой.

from circuitbreaker import circuit

@circuit(failure_threshold=5, recovery_timeout=60)
def call_bitrix(method: str, params: dict):
    return client.call(method, params)

После 5 ошибок подряд circuit breaker размыкается и сразу возвращает ошибку, не дожидаясь таймаута. Через 60 секунд пробует снова.

Логирование и отладка. Когда что-то идёт не так, нужно понимать, что именно. Логируйте все запросы и ответы, всегда и везде.

Идемпотентность. Операции должны быть безопасны для повторного выполнения. Получили дубль вебхука? Повторили запрос после таймаута? Результат должен быть тот же, что и при первом выполнении.


Итак, основные компоненты интеграции:

  • Прокси-слой для маршрутизации трафика между монолитом и микросервисами

  • Входящие вебхуки для вызова Битрикса из микросервисов (с троттлингом, retry и батчингом)

  • Исходящие вебхуки дл�� получения событий из Битрикса (с дедупликацией и верификацией)

  • Anti-Corruption Layer для изоляции доменных моделей

Начинайте с периферии (аналитика, уведомления), двигайтесь к ядру постепенно. Каждый этап должен быть обратимым.

Не пытайтесь мигрировать всё за месяц!

Если Битрикс24 для вас — не «админка», а платформа, стоит разложить его по слоям. Курс «Разработчик Битрикс24» про практику: развёртывание, события, модули и REST-приложения, бизнес-процессы и оптимизация — чтобы уверенно работать с legacy и кастомизациями без хаоса.

Чтобы узнать больше о формате обучения и познакомиться с преподавателями, приходите на бесплатные демо-уроки:

  • 9 февраля в 20:00. «Бизнес-процессы в Битрикс24, написание своих кастомных действий». Записаться

  • 19 февраля в 20:00. «Битрикс24 + MAX: разработка чат-ботов и автоматизация коммуникаций». Записаться