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

⸻

1. 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.

⸻

2. 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


⸻

3. 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/


⸻

4. 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


⸻

5. 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

⸻

6. 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


⸻

7. Forbidden Patterns

❌ Manual string prefixes ([ServiceName])
❌ Global/singleton loggers with mutable state
❌ any in logger abstractions
❌ Correlation IDs in Core
❌ Logging inside domain entities

⸻

8. 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