Skip to content

Coding standards

Audience: backend
Status: specced
Owns: backend
Depends on: Backend stack ADR, System architecture

One standard across Orchestrator, CAP, LMS, and WorkMasters. Lint/format/type-check are CI-gated (merge blocked on failure), not optional warnings.

src/
routes/ # thin — wire HTTP to controllers
controllers/ # parse/validate request, call service, shape response
services/ # business logic; orchestrates repositories + events
repositories/ # Prisma access only — no business logic
events/
emitters/ # internal (Node EventEmitter) domain events
publishers/ # outbox → RabbitMQ publisher
consumers/ # RabbitMQ consumers
grpc/
server/ # handlers this service exposes
clients/ # typed clients for calling other services
jobs/ # scheduled/cron work (e.g. OTP expiry, SLA gauges)
lib/ # shared internal utilities for this service
Package Purpose
@yourorg/common Logger, AppError hierarchy (incl. ScopeError), response envelope, cursor pagination, ULID, OTel bootstrap, outbox/event helpers, RBAC calling convention, Money / PhoneNumber
@yourorg/proto Generated gRPC stubs — IdentityService.VerifyIdentity, PaymentService.InitiatePayment (orchestrator.v1)
Shared ESLint + Prettier config Same rules in every repo; CI fails on violation
Husky (optional locally) Pre-commit lint/format/type-check — CI remains the real gate

Use class/strategy patterns where the architecture already needs them — not enterprise boilerplate by default:

Pattern Where
Strategy + Factory/Registry Workflow StageHandlerStrategy, payment/identity provider adapters
Repository Selectively — complex reused queries or hard-to-mock access; otherwise call Prisma from services
Manual DI Constructor / factory wiring at startup — no DI container required
Command-style Workflow actions (validate → execute → emit); not ordinary CRUD
Mechanism Scope Use for
EventEmitter Same process, same service Decouple in-process side effects after a domain change (audit row, cache invalidate, decide whether to write an outbox row)
RabbitMQ (via outbox) Across services Anything another service must see, and anything that must survive a crash

Rules of thumb:

  • Most notifications to users are eventually handled by Orchestrator — CAP/LMS emit domain events (outbox) that OL’s notification consumers handle; do not call SMTP from CAP.
  • EventEmitter listeners are lost on crash if they have not finished — never use them alone for “must publish” cross-service work.
  • Controllers should not call three unrelated services inline; prefer service-layer emit → local listeners / outbox write.
  1. ESLint + Prettier (shared config)
  2. TypeScript tsc --noEmit
  3. Unit tests
  4. OpenAPI / route alignment check where wired
  5. Integration tests against real Postgres + RabbitMQ in CI (compose), not only mocks