Skip to content

Design patterns

A catalog of the patterns actually used in the codebase — organized by pattern, with where each one lives and the decision or flow that explains it. This is a different cut of the same material as the ADRs (which are organized by decision) and the services map (organized by boundary): here the question is "what named technique is this, and where do I find it?".

Two rules kept this list honest: every entry points at real code, and patterns that look similar but aren't actually here (full Event Sourcing, Saga, Circuit Breaker) are called out in the last section rather than over-claimed — knowing when not to apply a pattern is the same skill as applying one.

Patterns on the architecture

flowchart LR
    C([Client]) -->|API Gateway| GW[Gateway]
    GW -->|Adapter / fail-fast| WO[Work Orders]
    GW -->|Composition + degradation| PR[Properties]
    subgraph WO_INT [inside Work Orders]
        SM[State Machine]
        OB[Transactional Outbox]
        RL[Polling Publisher]
        ST[Strategy seam: TriageClassifier]
    end
    WO --- OB
    OB -->|Guaranteed delivery| MQ{{Pub-Sub + Dead Letter}}
    OB -->|keyed log| KF[(Event log)]
    MQ -->|Competing + Idempotent Consumer| NT[Notifications]
    KF -->|Projection / Read Model| AU[Audit]
    ST -.-> AI[LLM]

Architectural & distributed patterns

Pattern What it is here Reference
API Gateway Single public entry point; auth, routing, composition — never owns data services (gateway)
Database per Service Each service owns its schema outright; cross-service refs are opaque ids, never FKs ADR-0001
Transactional Outbox Event row committed in the same transaction as the state change (outbox_events), closing the dual-write gap ADR-0007 · messaging/work-order-events.outbox.ts
Polling Publisher The relay drains the unpublished tail (FOR UPDATE SKIP LOCKED) to both brokers flow 2 · messaging/outbox-relay.ts
Event-Driven Architecture Side effects travel as domain events; producers and consumers never know each other flow 3
Projection / Read Model audit_events is a disposable materialized view over the Kafka log — rebuildable from offset 0 (a projection over an event log, not full event sourcing) flow 7 · audit/audit-ingest.service.ts
Publish-Subscribe Topic exchange fans one event out to independent consumers ADR-0002
Competing Consumers RabbitMQ work queues distribute messages across consumer instances ADR-0004
Dead Letter Channel + Retry TTL retry queue (5s × 3) then a .dlq parked for humans, with x-last-error flow 5 · notifications/messaging-resilience.ts
Idempotent Consumer Every consumer dedupes by eventIdON CONFLICT, triaged_at, processed-events set events (delivery)
Correlation Identifier x-request-id threads through HTTP and the async hop (correlationId in the envelope) ADR-0005 · libs/observability/src/request-context.ts
Health Check API Liveness (/health, no deps) vs readiness (/health/ready, checks deps) ADR-0005

Application & object patterns

Pattern What it is here Reference
Dependency Injection NestJS's backbone — every collaborator is injected, which is what makes the seams below testable throughout
Strategy (seam) An abstract class the code depends on, bound to a concrete impl: TriageClassifier, NotificationSender — swap provider = swap one binding ADR-0006 · triage/triage-classifier.ts
Repository + Data Mapper TypeORM repositories over entities; data-mapper (not active-record) keeps entities persistence-agnostic ADR-0003
State Machine Explicit allowed-transition table guards the work-order lifecycle flow 8 · work-orders/work-order-transitions.ts
Chain of Responsibility Guard chain: JwtAuthGuard (authn) then RolesGuard (authz) — 401 before 403 ADR-0008 · auth/jwt-auth.guard.ts
Decorator Custom route decorators @Public(), @Roles(), @CurrentUser() carry metadata the guards read auth/auth.decorators.ts
Interceptor (around-advice) The RED-metrics interceptor wraps every handler to time and label it libs/observability/src/metrics/http-metrics.interceptor.ts
Middleware The request-context middleware opens the ambient store before anything else runs libs/observability/src/request-context.middleware.ts
Ambient Context AsyncLocalStorage makes the request id and user id readable anywhere on the async path without threading params ADR-0005 · request-context.ts
Adapter DownstreamClient wraps fetch with the gateway's downstream policy (timeout, 502/504 translation, header propagation) gateway/http/downstream.client.ts
Factory buildDataSourceOptions, buildLoggerOptions, buildOpenApiDocument — one source of truth for wiring, shared by app and tooling work-orders-service/src/config/typeorm.config.ts
DTO + Validation Request DTOs with class-validator; response shapes as typed contracts (also the OpenAPI schema) api reference

Patterns deliberately NOT used (and why)

The judgment half of the catalog — each of these is a reasonable thing to reach for, and each was left out on purpose:

Pattern Why not When it would return
Full Event Sourcing The audit log is a projection, and services persist current state normally — not every state change is reconstructed from events If the domain needed temporal queries or per-aggregate replay as a first-class feature
Saga / Process Manager No multi-service transaction needs orchestration + compensation here; each event's effect is local and idempotent A cross-service workflow with rollback (e.g. reserve → charge → dispatch)
Circuit Breaker The gateway fails fast per request (3s timeout) but never opens a circuit — accepted, and documented A persistently slow/failing downstream under real load
CQRS (separate write model) Reads and writes share the same model per service; the audit projection is the only read-optimized view, and it's fed by events Read and write loads that need independently-scaled models
Service Registry (Consul/Eureka) The orchestrator is the registry — DNS + readiness gating Instances outside a single cluster, or registry-driven routing
Backends for Frontends One gateway serves all clients; there's no per-frontend backend Divergent client needs (mobile vs web) worth separate composition layers

References for the "not used" reasoning: service discovery in ADR-0009, the circuit-breaker gap in the phase-3 notes, and event-sourcing scope in ADR-0002.