DEV Community

kumaraish
kumaraish

Posted on

Logging Without the Noise: A Tiered Strategy for Clean Architecture

In the heat of development, logging is often the first thing we add and the last thing we standardize. We’ve all been there: staring at a production log stream where a single network glitch has triggered fifteen identical error messages across five different layers—or worse, a silent failure where the logs say absolutely nothing at all.

Effective logging is more than just a print statement with a timestamp; it is a semantic contract between your code and your future self. When logs are treated as an afterthought, they quickly devolve into "log pollution," where critical signals are buried under a mountain of DEBUG spam and duplicate stack traces.

To maintain a truly "Clean Architecture," we must treat logging with the same rigor as our business logic. This means defining clear ownership for every log entry and ensuring that errors move outward while logging happens only once. This guide outlines the formal rules of engagement for logging across different architectural layers, ensuring your logs provide high-fidelity diagnostic data without the deafening noise.

Why This Matters:

Searchability: Standardized logs mean faster debugging during outages.

Security: Explicit "What NOT to log" rules prevent PII (Personally Identifiable Information) leaks.

Clarity: By logging only at the "final call site," you eliminate the confusion caused by redundant error reports.

Logging Rules

Layer Responsibility What to Log What NOT to Log Log Level
UI / Compose Show user-friendly errors. No technical logging unless for debugging UI state. Optional Log.d() for UI actions when debugging. Stack traces, backend details, sensitive user data. DEBUG (optional)
ViewModel Final call site for logging. Catches exceptions from repository/use-case layer and logs them once. Operation context, exception type, stack trace (unless known/handled case), safe identifiers. Sensitive data, raw server responses. ERROR or WARN
Use Case / Interactor Translate repository errors into domain errors, propagate upwards. No logging unless critical business rule failed. Only critical domain-specific failures if unrecoverable at higher levels. All transient/expected exceptions (network blips, retries). ERROR (rare)
Repository Catch low-level exceptions, wrap into domain-specific errors, propagate upwards. No logging. Nothing by default. Everything (logging here will cause duplication). None
Data Source (API, DB) Throw exceptions up (wrapped if needed). No logging. Nothing by default. Same as repository — don’t log here. None
Library Modules Provide results or throw exceptions. Let caller log. Nothing except optional internal DEBUG logs in dev builds. Any production-facing logs. DEBUG only (optional)

🔥 Core Thumb Rules

  1. Log once, where handled

    • The ViewModel (or final business logic handler) is typically the final call site.
  2. Don’t log transient expected errors (like network timeouts if you retry automatically).

  3. Keep logs developer-focused

    • Technical detail + stack trace for devs, friendly plain language for users.
  4. Preserve cause when throwing

    • throw MyAppException("message", cause) so the final log still has the original stack trace.
  5. Never log sensitive data

    • Access tokens, passwords, personal identifiers.
  6. Use log levels meaningfully

    • DEBUG → Dev only
    • INFO → Rare, key events only
    • WARN → Recoverable problem
    • ERROR → Needs investigation

📌 Example Flow

  1. Library → throws DownloadException
  2. Repository → catches, wraps into NetworkError.DownloadFailed, rethrows
  3. ViewModel → catches NetworkError.DownloadFailed, logs stack trace once, updates UI state
  4. UI → shows “Could not fetch file. Please try again.”

Top comments (0)