Dependency Injection: Loose Coupling Pattern
Learn: Dependency Injection: Loose Coupling Pattern
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:
- Depend on abstractions, not concretions
- Inject dependencies through constructor, property, or method
- Use IoC containers for lifecycle management
- 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
| Tip | Benefit |
| Always depend on interfaces | Enables polymorphism and easy mocking |
| Use constructor injection | Makes dependencies explicit and required |
| Validate null dependencies | Fails fast with clear error messages |
| Choose appropriate lifetimes | Prevents memory leaks and state issues |
| Avoid service locator pattern | Keeps dependencies explicit and testable |
| Register at composition root | Centralizes configuration, easier maintenance |
| Use factory pattern for complex creation | Handles conditional logic and parameters |
| Keep interfaces focused | Follows Interface Segregation Principle |
| Avoid circular dependencies | Refactor to break cycles early |
| Document dependency requirements | Helps 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