DEV Community

Vimal
Vimal

Posted on

Designing for Dependency Injection — Think Like a Developer, Not a Container

Before reaching for .AddScoped or new, take a step back and ask what your class needs to do — not what it needs to create. This should model a functionality from the business domain.

Design Thinking First, DI Later

A well-designed class doesn't scream for injection. It simply:

  • Has a clear purpose.
  • Exposes what it needs to perform that purpose.
  • Delegates responsibilities it shouldn't own.

Let’s unpack that with a practical approach.


Step 1: Identify the Class's Responsibility

Ask: What behavior is this class responsible for? . Let us analyse an order processing functionality. How would you model the behavior and construct the class?

Example:

public class OrderProcessor
{
    public void ProcessOrder()
    {
        // Validate order
        // Charge payment
        // Send confirmation email
    }
}
Enter fullscreen mode Exit fullscreen mode

Looking at the above class, you’ve already spotted the problem: This class does too much. This is linked to the 'S' in SOLID principles, the class should handle only a single-responsibility.


Step 2: Delegate Responsibilities via Abstractions

Break it down. Let other services handle each concern.

public interface IOrderValidator
{
    bool Validate(Order order);
}

public interface IPaymentService
{
    void Charge(Order order);
}

public interface INotificationService
{
    void SendConfirmation(Order order);
}
Enter fullscreen mode Exit fullscreen mode

Now rewrite OrderProcessor as a coordinator, not a worker:

public class OrderProcessor
{
    private readonly IOrderValidator _validator;
    private readonly IPaymentService _payment;
    private readonly INotificationService _notifier;

    public OrderProcessor(
        IOrderValidator validator,
        IPaymentService payment,
        INotificationService notifier)
    {
        _validator = validator;
        _payment = payment;
        _notifier = notifier;
    }

    public void ProcessOrder(Order order)
    {
        if (!_validator.Validate(order)) return;

        _payment.Charge(order);
        _notifier.SendConfirmation(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

You’ve just designed for Dependency Injection — without thinking about the container.


Step 3: Make It Testable and Flexible

This class can now be tested using mocks:

var mockValidator = new Mock<IOrderValidator>();
mockValidator.Setup(v => v.Validate(It.IsAny<Order>())).Returns(true);

var mockPayment = new Mock<IPaymentService>();
var mockNotifier = new Mock<INotificationService>();

var processor = new OrderProcessor(mockValidator.Object, mockPayment.Object, mockNotifier.Object);
processor.ProcessOrder(new Order());
Enter fullscreen mode Exit fullscreen mode

✅ No real implementations required. ✅ You can test logic in isolation.


Step 4: Let the Container Wire It Up

Only now do you register it in .NET Core:

builder.Services.AddScoped<IOrderValidator, OrderValidator>();
builder.Services.AddScoped<IPaymentService, StripePaymentService>();
builder.Services.AddScoped<INotificationService, EmailNotifier>();
builder.Services.AddScoped<OrderProcessor>();
Enter fullscreen mode Exit fullscreen mode

Summary: The Design-First DI Checklist

Step Ask Yourself… Outcome
1 What does this class need to do? Define the class’s responsibility
2 What can be delegated to collaborators? Extract abstractions
3 How can this class be tested in isolation? Refactor for injectability
4 Where do I register this in DI container? Configure DI at the edges

📌 Remember

DI isn’t about using a container.
It’s about designing classes that ask for what they need, instead of building it themselves.

Start from behavior. Inject contracts. Let the container worry about construction.

Top comments (0)