Skip to content

ADR-0002. Mode-based deployment (FULL / API / WORKER)

FieldValue
StatusAccepted
Date2026-02-05
Deciderspayment-team, SRE
Supersedes

Context

  • Payment service handles two distinct workloads:
    1. REST + IPN ingestion — synchronous, latency-sensitive, scales with provider traffic.
    2. BullMQ workers — asynchronous (scheduler + confirmation), scales with queue depth.
  • Running both in one process limits independent scaling and amplifies crash blast radius.
  • However, dev environments need everything in one pod.

Decision

Single image, three deployment modes selected via APP_ENV_MQ_PAY_MODE:

ModeControllersQueue producerWorkersUse case
FULL (default)Dev — single pod
APIProduction REST tier — scale by request rate
WORKERProduction worker tier — scale by queue depth

Production pattern: 1× API + N× WORKER. Each WORKER pod must have a unique APP_ENV_NODE_ID (Snowflake worker ID = 91, 92, …).

Consequences

ProsCons
REST and worker scale independentlyThree deployment manifests to maintain
Worker crashes don't take down APIShared encryption key requirement (APP_ENV_APPLICATION_SECRET)
Dev still uses single pod (FULL)WORKER pods need unique snowflake IDs — error-prone
Standard "API + worker" topologyMode mismatch can cause silent dead queue

Alternatives Considered

OptionProsConsWhy rejected
Single mode (always full)Simpler opsCan't scale REST and workers independentlyWrong tradeoff for production
Separate API and WORKER imagesClear boundaryBuild pipeline complexity; image drift riskSingle-image-multi-mode is industry standard
Stateless workers without BullMQNo Redis dependencyNo retry / scheduler / persistencemq-pay needs queue semantics

References

  • @nx/mq-pay/src/common/constants.ts:11-14 (MQPayRunModes)
  • @nx/mq-pay/src/component.ts:437-444 (mode validation)
  • src/components/payment.component.ts:124-125 (mode resolution)

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.