Skip to main content

Command Palette

Search for a command to run...

Dependency Injection: Loose Coupling Pattern

Learn: Dependency Injection: Loose Coupling Pattern

Updated
4 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Dependency Injection: Loose Coupling Pattern

Problem

Tight Coupling Issues:

  • Classes directly instantiate their dependencies, creating hard-coded relationships
  • Testing becomes difficult because you can't easily mock dependencies
  • Changing implementations requires modifying multiple classes
  • Code reusability suffers due to inflexible dependencies
  • Maintenance becomes expensive as the codebase grows
// ❌ Tightly Coupled - Hard to test and maintain
public class OrderService
{
    private PaymentProcessor _paymentProcessor = new PaymentProcessor();
    private EmailNotifier _emailNotifier = new EmailNotifier();

    public void ProcessOrder(Order order)
    {
        _paymentProcessor.Process(order);
        _emailNotifier.SendConfirmation(order);
    }
}

Why This Fails:

  • Can't test without actual payment processing
  • Can't swap implementations without code changes
  • Dependencies are hidden and implicit
  • Violates Single Responsibility Principle

Solution

Dependency Injection Pattern:

  • Invert control by injecting dependencies from outside
  • Use abstractions (interfaces) instead of concrete implementations
  • Leverage IoC containers for automatic dependency resolution
  • Enable loose coupling and high testability

Key Principles:

  1. Depend on abstractions, not concretions
  2. Inject dependencies through constructor, property, or method
  3. Use IoC containers for lifecycle management
  4. Separate object creation from usage

Code Examples

1. Basic Dependency Injection (Constructor Injection)

// Define abstractions
public interface IPaymentProcessor
{
    void Process(Order order);
}

public interface IEmailNotifier
{
    void SendConfirmation(Order order);
}

// Concrete implementations
public class PaymentProcessor : IPaymentProcessor
{
    public void Process(Order order)
    {
        Console.WriteLine($"Processing payment for order {order.Id}");
    }
}

public class EmailNotifier : IEmailNotifier
{
    public void SendConfirmation(Order order)
    {
        Console.WriteLine($"Sending confirmation email for order {order.Id}");
    }
}

// Service with injected dependencies
public class OrderService
{
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly IEmailNotifier _emailNotifier;

    // Dependencies injected through constructor
    public OrderService(IPaymentProcessor paymentProcessor, IEmailNotifier emailNotifier)
    {
        _paymentProcessor = paymentProcessor ?? throw new ArgumentNullException(nameof(paymentProcessor));
        _emailNotifier = emailNotifier ?? throw new ArgumentNullException(nameof(emailNotifier));
    }

    public void ProcessOrder(Order order)
    {
        _paymentProcessor.Process(order);
        _emailNotifier.SendConfirmation(order);
    }
}

// Usage
var paymentProcessor = new PaymentProcessor();
var emailNotifier = new EmailNotifier();
var orderService = new OrderService(paymentProcessor, emailNotifier);
orderService.ProcessOrder(new Order { Id = 1 });

2. IoC Container - Microsoft.Extensions.DependencyInjection

using Microsoft.Extensions.DependencyInjection;

// Setup IoC Container
var services = new ServiceCollection();

// Register dependencies
services.AddScoped<IPaymentProcessor, PaymentProcessor>();
services.AddScoped<IEmailNotifier, EmailNotifier>();
services.AddScoped<OrderService>();

// Build service provider
var serviceProvider = services.BuildServiceProvider();

// Resolve and use
var orderService = serviceProvider.GetRequiredService<OrderService>();
orderService.ProcessOrder(new Order { Id = 1 });

3. Advanced IoC Configuration - Autofac

using Autofac;

// Build container
var builder = new ContainerBuilder();

// Register implementations
builder.RegisterType<PaymentProcessor>().As<IPaymentProcessor>();
builder.RegisterType<EmailNotifier>().As<IEmailNotifier>();
builder.RegisterType<OrderService>();

// Register with factory
builder.Register(c => new PaymentProcessor())
    .As<IPaymentProcessor>()
    .SingleInstance();

// Register with parameters
builder.RegisterType<OrderService>()
    .WithParameter("retryCount", 3);

var container = builder.Build();

// Resolve
using (var scope = container.BeginLifetimeScope())
{
    var orderService = scope.Resolve<OrderService>();
    orderService.ProcessOrder(new Order { Id = 1 });
}

4. Property Injection

public class OrderService
{
    public IPaymentProcessor PaymentProcessor { get; set; }
    public IEmailNotifier EmailNotifier { get; set; }

    public void ProcessOrder(Order order)
    {
        PaymentProcessor?.Process(order);
        EmailNotifier?.SendConfirmation(order);
    }
}

// Registration with property injection
services.AddScoped<OrderService>(provider =>
{
    var service = new OrderService
    {
        PaymentProcessor = provider.GetRequiredService<IPaymentProcessor>(),
        EmailNotifier = provider.GetRequiredService<IEmailNotifier>()
    };
    return service;
});

5. Method Injection

public class OrderService
{
    public void ProcessOrder(Order order, IPaymentProcessor paymentProcessor, IEmailNotifier emailNotifier)
    {
        paymentProcessor.Process(order);
        emailNotifier.SendConfirmation(order);
    }
}

// Usage
var orderService = new OrderService();
orderService.ProcessOrder(order, paymentProcessor, emailNotifier);

6. Testing with Mocks

using Moq;
using Xunit;

public class OrderServiceTests
{
    [Fact]
    public void ProcessOrder_CallsPaymentProcessor()
    {
        // Arrange
        var mockPaymentProcessor = new Mock<IPaymentProcessor>();
        var mockEmailNotifier = new Mock<IEmailNotifier>();
        var orderService = new OrderService(mockPaymentProcessor.Object, mockEmailNotifier.Object);
        var order = new Order { Id = 1 };

        // Act
        orderService.ProcessOrder(order);

        // Assert
        mockPaymentProcessor.Verify(x => x.Process(order), Times.Once);
        mockEmailNotifier.Verify(x => x.SendConfirmation(order), Times.Once);
    }
}

7. Lifetime Management

// Transient: New instance every time
services.AddTransient<IPaymentProcessor, PaymentProcessor>();

// Scoped: One instance per request/scope
services.AddScoped<IEmailNotifier, EmailNotifier>();

// Singleton: One instance for application lifetime
services.AddSingleton<ILogger, Logger>();

// Factory pattern
services.AddScoped<IPaymentProcessor>(provider =>
{
    var config = provider.GetRequiredService<IConfiguration>();
    return new PaymentProcessor(config["PaymentGateway"]);
});

Tips & Best Practices

TipBenefit
Always depend on interfacesEnables polymorphism and easy mocking
Use constructor injectionMakes dependencies explicit and required
Validate null dependenciesFails fast with clear error messages
Choose appropriate lifetimesPrevents memory leaks and state issues
Avoid service locator patternKeeps dependencies explicit and testable
Register at composition rootCentralizes configuration, easier maintenance
Use factory pattern for complex creationHandles conditional logic and parameters
Keep interfaces focusedFollows Interface Segregation Principle
Avoid circular dependenciesRefactor to break cycles early
Document dependency requirementsHelps team understand architecture

Anti-Patterns to Avoid:

  • ❌ Service Locator (hiding dependencies)
  • ❌ Static dependencies
  • ❌ God objects with too many dependencies
  • ❌ Mixing concerns in constructors
  • ❌ Registering concrete types directly

When to Use DI:

  • ✅ Multi-layered applications
  • ✅ Projects requiring testability
  • ✅ Large teams with changing requirements
  • ✅ Microservices architectures
  • ✅ Any production application