How do I effectively design my application where most classes depend on ILogger?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
If most classes in an application need logging, that is usually normal rather than a design smell. The goal is not to avoid ILogger, but to inject it in a way that keeps business code focused, avoids boilerplate, and preserves structured, queryable logs.
Prefer constructor injection with typed loggers
In modern .NET applications, the standard pattern is constructor injection with ILogger<T>. That gives each class a logger category based on its type.
This is better than a shared global logger because the category becomes meaningful in log sinks and filters.
Register logging once in composition root
Your application should configure logging centrally and let dependency injection supply loggers everywhere else.
That keeps logging infrastructure in one place instead of scattering logger creation across the codebase.
Do not wrap ILogger without a reason
Many teams create their own IMyLogger abstraction immediately. That is often unnecessary. ILogger is already an abstraction.
A wrapper is worth it only when you need:
- domain-specific logging methods
- cross-cutting enrichment rules
- portability away from the Microsoft logging ecosystem
If you just forward Info, Warn, and Error calls, the wrapper usually adds noise without adding value.
Use scopes and structured logging
Good application design is not only about injecting the logger. It is also about logging rich context consistently.
This is more valuable than passing string prefixes around manually.
Keep logging out of pure domain objects when possible
Not every class should log directly. Entities, value objects, and simple algorithm classes often become cleaner if logging stays at the application-service or workflow boundary.
A useful guideline:
- orchestration services can log
- infrastructure adapters can log
- controllers and handlers can log
- simple domain models usually should not
That keeps logging where decisions and side effects happen.
Avoid service locator patterns
If every class needs logging, it may be tempting to grab a logger from a static provider. Avoid that. Constructor injection remains preferable because:
- dependencies stay visible
- tests are simpler
- classes are easier to reason about
For tests, you can use NullLogger<T> when logging output does not matter:
That makes test setup lightweight without changing the production design.
When cross-cutting logging should move elsewhere
If many classes only log entry, exit, and timing, consider middleware, decorators, or pipeline behaviors instead of putting those log lines in every method manually.
Examples:
- HTTP request logging middleware
- MediatR pipeline behaviors
- decorator classes around repositories or handlers
That reduces repetition and makes log formatting more consistent.
Common Pitfalls
The most common mistake is treating widespread logger injection as proof that the architecture is wrong. Usually the real issue is poor logging strategy, not logger presence. Another is wrapping ILogger in a custom interface that adds no domain value. Teams also often log unstructured strings and then discover later that the logs cannot be queried effectively. Static global logger access is another frequent problem because it hides dependencies and makes tests harder. Finally, developers sometimes push logging into every low-level class, including simple domain types, which creates noise and distracts from actual business behavior.
Summary
- Inject
ILogger<T>through constructors and configure logging centrally. - Use typed loggers so categories are meaningful.
- Prefer structured logging and scopes over hand-built log strings.
- Keep simple domain objects free of unnecessary logging when possible.
- Use decorators or middleware for repetitive cross-cutting log behavior.
- Wrap
ILoggeronly when you need real domain-specific logging semantics.

