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.
| 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
-
Log once, where handled
- The ViewModel (or final business logic handler) is typically the final call site.
Don’t log transient expected errors (like network timeouts if you retry automatically).
-
Keep logs developer-focused
- Technical detail + stack trace for devs, friendly plain language for users.
-
Preserve cause when throwing
-
throw MyAppException("message", cause)so the final log still has the original stack trace.
-
-
Never log sensitive data
- Access tokens, passwords, personal identifiers.
-
Use log levels meaningfully
-
DEBUG→ Dev only -
INFO→ Rare, key events only -
WARN→ Recoverable problem -
ERROR→ Needs investigation
-
📌 Example Flow
-
Library → throws
DownloadException -
Repository → catches, wraps into
NetworkError.DownloadFailed, rethrows -
ViewModel → catches
NetworkError.DownloadFailed, logs stack trace once, updates UI state - UI → shows “Could not fetch file. Please try again.”

Top comments (0)