Вопрос открыт. Сейчас в коде и то, и другое: фаза recognize построена как один оркестратор (step.invoke-цепочка), а всё ниже по потоку (interpret → enrichments → save-FHIR-AI → doctor-review → PDF) — как choreography (функции слушают события друг друга). Граница между этими режимами проведена «исторически», не по принципу. Надо решить: расширять orchestration вниз, оставлять текущий split и кодифицировать критерий границы, или наоборот уходить в choreography. Это не общий вопрос «оркестрация vs хореография» в вакууме (теория — на inngest § Orchestration vs Choreography), а конкретно про наш пайплайн на Inngest.

Контекст — что сейчас в коде

Фаза recognize — чистая orchestration. recognize.orchestrator.ts (триггер input/recognize.uploaded; легаси-движок — recognize.legacy.orchestrator.ts, input/recognize.legacy.uploaded) явно построен как один createFunction, который через step.invoke дёргает функции-этапы и передаёт данные между ними: image-crosscheckfhir-contextnormalize-and-harmonize / normalize / loinc-harmonizationrangessave-fhir-preliminarysave-fhir-enrichwebhook. Каждый этап — отдельная createFunction (свои retries/concurrency), но порядок и поток жёстко прописаны в оркестраторе. В файле есть комментарий: «Uses step.invoke() to call each function and pass data between them».

Ниже по потоку — choreography. interpretation-analysis.function.ts завершается step.sendEvent (analysis/...complete); дальше всё расходится событиями: enrichment/overview.requested / enrichment/follow-up.requested / enrichment/panel-overview.requested / enrichment/trends.requested / enrichment/product-recommendations.requested (и их patient.-варианты для V2.5 patient-scoped трека), enrichment/triggered, fhir/ai-resources.save, doctor/review.pending, interpretation/completed, дальше PDF-генерация. Это fan-out — одно «анализ готов» порождает N независимых enrichment-функций; никто не держит «весь поток» в одном месте.

Плюс ещё пара custom-механизмов рядом. analysis-queue-gate.function.ts (событие analysis/queue.enqueue) — гейт с собственной очередью поверх Inngest; doctor-review-gate.function.tswaitForEvent-гейт для HITL. Provisioning (org/created) и input-ingestion (input/json.uploaded / input/pdf.uploaded / HL7-bucket) — отдельные event-triggered функции, не часть оркестратора.

Итог: граница «orchestration → choreography» проходит примерно по линии recognize | interpret. Это сложилось, не выбрано. CriticMarkup Ильдара (May 11): «как между собой оркестрировать — можно через события отправлять друг другу, а можно сделать один оркестратор большой; у нас так и так сделано и непонятно, как лучше — надо подумать».

Позиции

A. Один большой оркестратор на весь пайплайн

Весь поток (recognize → normalize → LOINC → ranges → save-fhir → interpret → enrichments → save-FHIR-AI → PDF) — одна createFunction, цепочка step.invoke / step.run, fan-out enrichment’ов — через Promise.all([step.invoke(...), ...]) внутри неё.

  • За: вся логика потока в одном файле, видно «что за чем»; есть queryable state — Inngest-run и есть «статус анализа X» (для UI / monitoring / DQD); один владелец процесса; ретраи/concurrency на этапах настраиваются как сейчас (этапы — отдельные функции, вызванные через invoke).
  • Против: лимит 1000 шагов на функцию — параметр-анализ на больших панелях (N параметров × M шагов на параметр) может в него упереться → пришлось бы всё равно выносить часть в дочерние функции / fan-out; failure любого этапа = ретрай (и replay) всего оркестратора; добавить этап = трогать оркестратор; кросс-сервисная граница (recognition / fhir-services / analysis-worker — разные деплои/Inngest-app’ы) плохо ложится на step.invoke (invoke — в пределах одного app/registration; между app’ами — события).

B. Чистая choreography

Каждый этап — отдельная функция, слушает событие предыдущего, эмитит следующее; «весь поток» не материализован нигде.

  • За: слабая связность; fan-out естественен; failure локализован одной функцией; нет лимита 1000 шагов на «весь пайплайн»; разные «владельцы» этапов; кросс-сервисно работает само (события — единственный механизм, который и так пересекает app’ы).
  • Против: control flow неочевиден — «где сейчас анализ X / почему застрял» = собирай по событиям и логам; отлаживать тяжелее; состояние размазано; нет единого queryable «статуса pipeline-run»; Maxim Fateev (создатель Temporal): «Events and queues … very good runtime abstractions, but as a design choice they create very brutal systems because everything is connected to everything and there are no clear APIs»; Temporal-блог: choreography «can make control flow unclear», «challenging to debug».

C. Гибрид с явной границей (вариант текущего, но осознанный)

Оркестратор для последовательного хребта, choreography для fan-out независимых реакций, waitForEvent для HITL. Это де-факто то, что есть; вопрос — где провести границу по принципу, а не по истории. Кандидат-критерий: оркестрация там, где (а) этапы строго последовательны (каждый зависит от output предыдущего), (б) нужен queryable state, (в) суммарно укладываемся в 1000 шагов, (г) всё в одном сервисе/app; choreography — где (а) реакции независимы / параллельны, (б) разные владельцы, (в) кросс-сервисная граница, (г) есть риск упереться в лимит шагов. По этому критерию: recognize-хребет — оркестрация (как сейчас); параметр-анализ N×M — скорее fan-out (упирается в лимит); enrichments — choreography (как сейчас); межсервисные стыки — события.

Что нужно для разрешения

  • Перечислить этапы пайплайна и для каждого ответить: строго последователен или независим/параллелизуем? один сервис или кросс-сервисно? сколько Inngest-шагов порождает (риск 1000)? нужен ли его статус в UI/monitoring?
  • Решить, нужен ли вообще единый «pipeline-run» с queryable-статусом — или статус анализа собирается из FHIR-ресурсов (что записано) + Inngest UI (где сейчас выполнение). Если первое — это сильный аргумент за orchestration на хребте.
  • Проверить cross-app поведение Inngest: step.invoke действительно ограничен одним app/registration? Если да — межсервисные переходы вынужденно через события, и вопрос сводится к «orchestration внутри сервиса, choreography между».
  • Учесть Inngest roadmap: per-step options (override retry/concurrency на уровне шага) — когда выйдут, отпадёт часть аргументов «дочерняя функция ради своей config», часть invoke-разбиений можно будет схлопнуть в step.run. Checkpointing (developer preview) — снижает стоимость длинных step.run-цепочек (−50% inter-step latency), делает большой оркестратор дешевле.
  • Биллинг: step.invoke создаёт ещё один run (оркестратор + N invoke = N+1 run); choreography через sendEvent — тоже N runs. По стоимости примерно одинаково — разница в наблюдаемости и связности, не в деньгах. Не использовать «дороже/дешевле» как аргумент без проверки.
  • Решить судьбу analysis-queue-gate (собственная очередь поверх Inngest на analysis/queue.enqueue) — это смежный self-rolled-queue запах (no-self-rolled-queues); нужен ли он или Inngest flow control (concurrency keyed, throttle, singleton) покрывает.

Следствия

  • Пока не решено — новые этапы добавляются «как у соседнего» (recognize-этап → ещё один step.invoke в оркестраторе; downstream-этап → ещё одно событие), drift растёт.
  • Порт validity-классификатора — частный случай этого же вопроса: его внутренний батч-fan-out (classifyPatient → ~ceil(N/8) LLM-вызовов через worker-pool) при переразложении на Inngest должен стать чем — Promise.all([step.run("batch-i", …)]) внутри одной функции? дочерней функцией step.invoke ×ceil(N/8)? одним шагом? — это решается тем же критерием границы. См. biomarker-actuality-integration § Inngest-декомпозиция (Вопрос 3).
  • Смежная боль: словарь стадий пайплайна несогласован (recognize ×2 движка, «summary» vs «analysis» vs «interpret», 5× «save-fhir-»/«enrich-fhir-») — health-report-vocabulary. Решение про композицию и решение про именование стоит делать вместе (граница orchestration/choreography = естественная линия для согласованного словаря).

Открытые вопросы

  • Где провести границу orchestration/choreography по принципу — критерий из позиции C достаточен или есть кейсы, которые он не покрывает?
  • Cross-app step.invoke — допустим ли вообще? (TBD verify по Inngest-докам / нашему registration-устройству.)
  • Нужен ли единый queryable «pipeline-run» с прогрессом — или достаточно FHIR-ресурсов + Inngest UI?
  • analysis-queue-gate — оставить или убрать в пользу нативного flow control Inngest?

Связано

  • inngest § Orchestration vs Choreography — теория (push/pull, три инструмента, что говорят Temporal/Fateev/Inngest), § Передача данных между шагами — FHIR-store как state-store
  • no-self-rolled-queues — оркестрация/очереди/concurrency — через Inngest, не самодельные; этот вопрос — про то, как именно раскладывать пайплайн на Inngest-примитивы
  • biomarker-actuality-integration § Inngest-декомпозиция — конкретный кейс того же вопроса (батч = шаг? fan-out? функция?)
  • health-report-vocabulary — словарь стадий несогласован; решать вместе с композицией
  • llm-proxy-choice — аналог: model-routing/fallback вынесен на инфраструктурный слой (Bifrost), а не раскатан по коду — тот же принцип, что «оркестрация — на Inngest, не руками»

Источники

Источники: 1.

Сноски

  1. [Temporal: Orchestrate or Choreograph Your Saga](, accessed 2026-05-17, https://temporal.io/blog/to-choreograph-or-orchestrate-your-saga-that-is-the-question — · SE Radio 596: Maxim Fateev on Durable Execution.