Application Design
Dependency Injection
ILogger
Software Architecture
Logging Framework

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.

csharp
1using Microsoft.Extensions.Logging;
2
3public sealed class InvoiceService
4{
5    private readonly ILogger<InvoiceService> _logger;
6
7    public InvoiceService(ILogger<InvoiceService> logger)
8    {
9        _logger = logger;
10    }
11
12    public void CreateInvoice(int customerId)
13    {
14        _logger.LogInformation("Creating invoice for customer {CustomerId}", customerId);
15    }
16}

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.

csharp
1using Microsoft.Extensions.DependencyInjection;
2using Microsoft.Extensions.Hosting;
3using Microsoft.Extensions.Logging;
4
5var builder = Host.CreateApplicationBuilder(args);
6
7builder.Logging.ClearProviders();
8builder.Logging.AddConsole();
9
10builder.Services.AddTransient<InvoiceService>();
11
12using var host = builder.Build();
13
14var service = host.Services.GetRequiredService<InvoiceService>();
15service.CreateInvoice(42);

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.

csharp
1using Microsoft.Extensions.Logging;
2
3public sealed class PaymentService
4{
5    private readonly ILogger<PaymentService> _logger;
6
7    public PaymentService(ILogger<PaymentService> logger)
8    {
9        _logger = logger;
10    }
11
12    public void CapturePayment(string orderId, decimal amount)
13    {
14        using var scope = _logger.BeginScope(new Dictionary<string, object>
15        {
16            ["OrderId"] = orderId
17        });
18
19        _logger.LogInformation("Capturing payment for amount {Amount}", amount);
20    }
21}

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:

csharp
using Microsoft.Extensions.Logging.Abstractions;

var service = new InvoiceService(NullLogger<InvoiceService>.Instance);

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 ILogger only when you need real domain-specific logging semantics.

Course illustration
Course illustration

All Rights Reserved.