4.0 KiB
Logging & Correlation ID Design Guide (Clean Architecture)
This document defines a clean, strict, and production-ready logging architecture with correlation IDs.
It removes ambiguity around: • where logging belongs • how context is attached • how logs stay machine-readable • how Clean Architecture boundaries are preserved
The rules below are non-negotiable.
⸻
Core Principles
Logs are data, not text. Context is injected, never constructed in the Core.
Logging must: • be machine-readable • be framework-agnostic in the Core • support correlation across requests • be safe for parallel execution
⸻
Architectural Responsibilities
Core • describes intent • never knows about correlation IDs • never knows about log destinations
App Layer (API) • defines runtime context (request, user) • binds correlation IDs
Adapters • implement concrete loggers (console, file, structured) • decide formatting and transport
⸻
- Logger Port (Core)
Purpose
Defines what logging means, without defining how logging works.
⸻
Rules • exactly one logging interface • no framework imports • no correlation or runtime context
⸻
Location
core/shared/application/LoggerPort.ts
⸻
Contract • debug(message, meta?) • info(message, meta?) • warn(message, meta?) • error(message, meta?)
Messages are semantic. Metadata is optional and structured.
⸻
- Request Context (App Layer)
Purpose
Represents runtime execution context.
⸻
Contains • correlationId • optional userId
⸻
Rules • never visible to Core • created per request
⸻
Location
apps/api/context/RequestContext.ts
⸻
- Logger Implementations (Adapters)
Purpose
Provide concrete logging behavior.
⸻
Rules • implement LoggerPort • accept context via constructor • produce structured logs • no business logic
⸻
Examples • ConsoleLogger • FileLogger • PinoLogger • LokiLogger
⸻
Location
adapters/logging/
⸻
- Logger Factory
Purpose
Creates context-bound logger instances.
⸻
Rules • factory is injected • logger instances are short-lived • component name is bound here
⸻
Location
adapters/logging/LoggerFactory.ts adapters/logging/LoggerFactoryImpl.ts
⸻
- Correlation ID Handling
Where it lives • API middleware • message envelopes • background job contexts
⸻
Rules • generated once per request • propagated across async boundaries • never generated in the Core
⸻
- Usage Rules by Layer
Layer Logging Allowed Notes Domain ❌ No Throw domain errors instead Use Cases ⚠️ Minimal Business milestones only API Services ✅ Yes Main logging location Adapters ✅ Yes IO, integration, failures Frontend ⚠️ Limited Errors + analytics only
⸻
- Forbidden Patterns
❌ Manual string prefixes ([ServiceName]) ❌ Global/singleton loggers with mutable state ❌ any in logger abstractions ❌ Correlation IDs in Core ❌ Logging inside domain entities
⸻
- File Structure (Final)
core/ └── shared/ └── application/ └── LoggerPort.ts # * required
apps/api/ ├── context/ │ └── RequestContext.ts # * required ├── middleware/ │ └── CorrelationMiddleware.ts └── modules/ └── */ └── *Service.ts
adapters/ └── logging/ ├── LoggerFactory.ts # * required ├── LoggerFactoryImpl.ts # * required ├── ConsoleLogger.ts # optional ├── FileLogger.ts # optional └── PinoLogger.ts # optional
⸻
Mental Model (Final)
The Core describes events. The App provides context. Adapters deliver telemetry.
If any layer violates this, the architecture is broken.
⸻
Summary • one LoggerPort in the Core • context bound outside the Core • adapters implement logging destinations • correlation IDs are runtime concerns • logs are structured, searchable, and safe
This setup is: • Clean Architecture compliant • production-ready • scalable • refactor-safe