👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Constructor Over-injection Antipattern in Dependency Injection

Constructor Over-injection Antipattern in Dependency Injection

Author - Abdul Rahman (Bhai)

Dependency Injection

7 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

The Constructor Over-injection anti-pattern occurs when a class requires too many dependencies through its constructor, making it unwieldy and indicating potential design problems. While dependency injection promotes explicit dependencies, having too many can signal that a class is doing too much. In this article, let's learn about the Constructor Over-injection anti-pattern in .NET and understand when many dependencies become a problem and how to address it.

Why we gonna do?

The Constructor Over-injection anti-pattern is characterized by constructors that accept an excessive number of dependencies - typically more than 3-4 parameters. While there's no hard rule about the exact number, when a constructor becomes unwieldy with many parameters, it often indicates that the class is violating the Single Responsibility Principle and trying to do too much.

Unlike other anti-patterns, Constructor Over-injection is more of a design smell that suggests deeper architectural issues. The problem isn't with dependency injection itself, but rather with the class design that necessitates so many dependencies.

Why Constructor Over-injection is Problematic

Constructor Over-injection creates several issues:

  • Single Responsibility Principle violation - Class is likely doing too much
  • Complex object creation - Difficult to instantiate and test
  • Maintenance difficulties - Changes require updating many dependencies
  • Cognitive overload - Hard to understand what the class actually does
  • Tight coupling - Class depends on many other components
  • Testing complexity - Need to mock many dependencies for tests

When Many Dependencies Might Be Acceptable

Before jumping to refactoring, consider that some scenarios legitimately require multiple dependencies:

  • Facade classes - Coordinating multiple subsystems
  • Composition roots - Application entry points
  • Integration services - Connecting disparate systems
  • Workflow orchestrators - Managing complex business processes

How we gonna do?

Example: OrderService with Constructor Over-injection

Here's a typical example of Constructor Over-injection anti-pattern:


// Constructor Over-injection anti-pattern
public class OrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly ICustomerRepository customerRepository;
    private readonly IProductRepository productRepository;
    private readonly IInventoryService inventoryService;
    private readonly IPricingService pricingService;
    private readonly IPaymentService paymentService;
    private readonly IShippingService shippingService;
    private readonly INotificationService notificationService;
    private readonly ITaxCalculator taxCalculator;
    private readonly IDiscountCalculator discountCalculator;
    private readonly IAuditLogger auditLogger;
    private readonly IEmailService emailService;
    private readonly IUserContext userContext;
    
    // Constructor with too many dependencies (13 parameters!)
    public OrderService(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        IProductRepository productRepository,
        IInventoryService inventoryService,
        IPricingService pricingService,
        IPaymentService paymentService,
        IShippingService shippingService,
        INotificationService notificationService,
        ITaxCalculator taxCalculator,
        IDiscountCalculator discountCalculator,
        IAuditLogger auditLogger,
        IEmailService emailService,
        IUserContext userContext)
    {
        this.orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
        this.customerRepository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
        this.productRepository = productRepository ?? throw new ArgumentNullException(nameof(productRepository));
        this.inventoryService = inventoryService ?? throw new ArgumentNullException(nameof(inventoryService));
        this.pricingService = pricingService ?? throw new ArgumentNullException(nameof(pricingService));
        this.paymentService = paymentService ?? throw new ArgumentNullException(nameof(paymentService));
        this.shippingService = shippingService ?? throw new ArgumentNullException(nameof(shippingService));
        this.notificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
        this.taxCalculator = taxCalculator ?? throw new ArgumentNullException(nameof(taxCalculator));
        this.discountCalculator = discountCalculator ?? throw new ArgumentNullException(nameof(discountCalculator));
        this.auditLogger = auditLogger ?? throw new ArgumentNullException(nameof(auditLogger));
        this.emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
        this.userContext = userContext ?? throw new ArgumentNullException(nameof(userContext));
    }
    
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // This method uses nearly all injected dependencies
        var currentUser = userContext.GetCurrentUserId();
        var customer = await customerRepository.GetByIdAsync(request.CustomerId);
        
        auditLogger.LogOrderCreationStarted(currentUser, request.CustomerId);
        
        var orderItems = new List<OrderItem>();
        decimal subtotal = 0;
        
        foreach (var item in request.Items)
        {
            var product = await productRepository.GetByIdAsync(item.ProductId);
            if (!await inventoryService.IsAvailableAsync(item.ProductId, item.Quantity))
                throw new InsufficientInventoryException($"Product {item.ProductId} not available");
            
            var price = await pricingService.GetPriceAsync(item.ProductId, item.Quantity);
            var discount = await discountCalculator.CalculateDiscountAsync(customer, product, item.Quantity);
            var itemTotal = (price * item.Quantity) - discount;
            
            orderItems.Add(new OrderItem
            {
                ProductId = item.ProductId,
                Quantity = item.Quantity,
                UnitPrice = price,
                Discount = discount,
                Total = itemTotal
            });
            
            subtotal += itemTotal;
        }
        
        var tax = await taxCalculator.CalculateTaxAsync(customer.Address, subtotal);
        var shipping = await shippingService.CalculateShippingAsync(customer.Address, orderItems);
        var total = subtotal + tax + shipping;
        
        var order = new Order
        {
            CustomerId = customer.Id,
            Items = orderItems,
            Subtotal = subtotal,
            Tax = tax,
            Shipping = shipping,
            Total = total,
            CreatedBy = currentUser,
            CreatedAt = DateTime.UtcNow
        };
        
        // Process payment
        var paymentResult = await paymentService.ProcessPaymentAsync(order.Total, request.PaymentDetails);
        if (!paymentResult.IsSuccessful)
            throw new PaymentFailedException(paymentResult.ErrorMessage);
        
        order.PaymentId = paymentResult.PaymentId;
        
        // Save order
        var savedOrder = await orderRepository.SaveAsync(order);
        
        // Send notifications
        await notificationService.NotifyOrderCreatedAsync(savedOrder);
        await emailService.SendOrderConfirmationAsync(customer.Email, savedOrder);
        
        auditLogger.LogOrderCreated(savedOrder);
        
        return savedOrder;
    }
}
            

Problems with Constructor Over-injection

1. Unwieldy Constructor

The constructor is extremely long and difficult to work with. When creating instances for testing, you need to provide 13 different dependencies.

2. Single Responsibility Principle Violation

The OrderService is clearly doing too much - it handles validation, pricing, tax calculation, payment processing, inventory management, notifications, and auditing.

3. Complex Testing

Unit tests become unwieldy because you must mock all dependencies:


[Test]
public async Task CreateOrderAsync_WithValidRequest_CreatesOrder()
{
    // Arrange - Must create mocks for 13 dependencies!
    var mockOrderRepo = new Mock<IOrderRepository>();
    var mockCustomerRepo = new Mock<ICustomerRepository>();
    var mockProductRepo = new Mock<IProductRepository>();
    var mockInventory = new Mock<IInventoryService>();
    var mockPricing = new Mock<IPricingService>();
    var mockPayment = new Mock<IPaymentService>();
    var mockShipping = new Mock<IShippingService>();
    var mockNotification = new Mock<INotificationService>();
    var mockTaxCalc = new Mock<ITaxCalculator>();
    var mockDiscountCalc = new Mock<IDiscountCalculator>();
    var mockAuditLogger = new Mock<IAuditLogger>();
    var mockEmailService = new Mock<IEmailService>();
    var mockUserContext = new Mock<IUserContext>();
    
    // Setup all the mocks... (many lines of setup code)
    
    var orderService = new OrderService(
        mockOrderRepo.Object,
        mockCustomerRepo.Object,
        mockProductRepo.Object,
        mockInventory.Object,
        mockPricing.Object,
        mockPayment.Object,
        mockShipping.Object,
        mockNotification.Object,
        mockTaxCalc.Object,
        mockDiscountCalc.Object,
        mockAuditLogger.Object,
        mockEmailService.Object,
        mockUserContext.Object
    );
    
    // Act & Assert...
}
            

Refactoring Strategy 1: Extract Smaller Services

Break down the large service into smaller, focused services:


// Extract order calculation logic
public class OrderCalculationService
{
    private readonly IPricingService pricingService;
    private readonly ITaxCalculator taxCalculator;
    private readonly IDiscountCalculator discountCalculator;
    private readonly IShippingService shippingService;
    
    public OrderCalculationService(
        IPricingService pricingService,
        ITaxCalculator taxCalculator,
        IDiscountCalculator discountCalculator,
        IShippingService shippingService)
    {
        this.pricingService = pricingService;
        this.taxCalculator = taxCalculator;
        this.discountCalculator = discountCalculator;
        this.shippingService = shippingService;
    }
    
    public async Task<OrderCalculation> CalculateOrderAsync(Customer customer, OrderItem[] items)
    {
        decimal subtotal = 0;
        
        foreach (var item in items)
        {
            var price = await pricingService.GetPriceAsync(item.ProductId, item.Quantity);
            var discount = await discountCalculator.CalculateDiscountAsync(customer, item.Product, item.Quantity);
            item.UnitPrice = price;
            item.Discount = discount;
            item.Total = (price * item.Quantity) - discount;
            subtotal += item.Total;
        }
        
        var tax = await taxCalculator.CalculateTaxAsync(customer.Address, subtotal);
        var shipping = await shippingService.CalculateShippingAsync(customer.Address, items);
        
        return new OrderCalculation
        {
            Subtotal = subtotal,
            Tax = tax,
            Shipping = shipping,
            Total = subtotal + tax + shipping
        };
    }
}

// Extract order validation logic
public class OrderValidationService
{
    private readonly IProductRepository productRepository;
    private readonly IInventoryService inventoryService;
    
    public OrderValidationService(
        IProductRepository productRepository,
        IInventoryService inventoryService)
    {
        this.productRepository = productRepository;
        this.inventoryService = inventoryService;
    }
    
    public async Task ValidateOrderAsync(CreateOrderRequest request)
    {
        foreach (var item in request.Items)
        {
            var product = await productRepository.GetByIdAsync(item.ProductId);
            if (product == null)
                throw new ProductNotFoundException($"Product {item.ProductId} not found");
                
            if (!await inventoryService.IsAvailableAsync(item.ProductId, item.Quantity))
                throw new InsufficientInventoryException($"Product {item.ProductId} not available");
        }
    }
}

// Extract order processing logic  
public class OrderProcessingService
{
    private readonly IPaymentService paymentService;
    private readonly INotificationService notificationService;
    private readonly IEmailService emailService;
    private readonly IAuditLogger auditLogger;
    
    public OrderProcessingService(
        IPaymentService paymentService,
        INotificationService notificationService,
        IEmailService emailService,
        IAuditLogger auditLogger)
    {
        this.paymentService = paymentService;
        this.notificationService = notificationService;
        this.emailService = emailService;
        this.auditLogger = auditLogger;
    }
    
    public async Task<string> ProcessPaymentAsync(decimal amount, PaymentDetails paymentDetails)
    {
        var paymentResult = await paymentService.ProcessPaymentAsync(amount, paymentDetails);
        if (!paymentResult.IsSuccessful)
            throw new PaymentFailedException(paymentResult.ErrorMessage);
            
        return paymentResult.PaymentId;
    }
    
    public async Task SendNotificationsAsync(Customer customer, Order order)
    {
        await notificationService.NotifyOrderCreatedAsync(order);
        await emailService.SendOrderConfirmationAsync(customer.Email, order);
        auditLogger.LogOrderCreated(order);
    }
}
            

Refactored OrderService

Now the main service becomes much simpler and focused:


// Refactored OrderService with fewer, more focused dependencies
public class OrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly ICustomerRepository customerRepository;
    private readonly OrderValidationService validationService;
    private readonly OrderCalculationService calculationService;
    private readonly OrderProcessingService processingService;
    private readonly IUserContext userContext;
    
    // Much cleaner constructor with only 6 dependencies
    public OrderService(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        OrderValidationService validationService,
        OrderCalculationService calculationService,
        OrderProcessingService processingService,
        IUserContext userContext)
    {
        this.orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
        this.customerRepository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
        this.validationService = validationService ?? throw new ArgumentNullException(nameof(validationService));
        this.calculationService = calculationService ?? throw new ArgumentNullException(nameof(calculationService));
        this.processingService = processingService ?? throw new ArgumentNullException(nameof(processingService));
        this.userContext = userContext ?? throw new ArgumentNullException(nameof(userContext));
    }
    
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        var currentUser = userContext.GetCurrentUserId();
        var customer = await customerRepository.GetByIdAsync(request.CustomerId);
        
        // Validate the order
        await validationService.ValidateOrderAsync(request);
        
        // Calculate pricing, tax, shipping
        var orderItems = CreateOrderItems(request.Items);
        var calculation = await calculationService.CalculateOrderAsync(customer, orderItems);
        
        // Create the order
        var order = new Order
        {
            CustomerId = customer.Id,
            Items = orderItems,
            Subtotal = calculation.Subtotal,
            Tax = calculation.Tax,
            Shipping = calculation.Shipping,
            Total = calculation.Total,
            CreatedBy = currentUser,
            CreatedAt = DateTime.UtcNow
        };
        
        // Process payment
        order.PaymentId = await processingService.ProcessPaymentAsync(order.Total, request.PaymentDetails);
        
        // Save order
        var savedOrder = await orderRepository.SaveAsync(order);
        
        // Send notifications
        await processingService.SendNotificationsAsync(customer, savedOrder);
        
        return savedOrder;
    }
    
    private OrderItem[] CreateOrderItems(CreateOrderItemRequest[] items)
    {
        return items.Select(item => new OrderItem
        {
            ProductId = item.ProductId,
            Quantity = item.Quantity
        }).ToArray();
    }
}
            

Refactoring Strategy 2: Facade Pattern with Aggregate Services

Another approach is to create aggregate services that group related functionality:


// Aggregate service for all order-related operations
public interface IOrderOperations
{
    Task ValidateOrderAsync(CreateOrderRequest request);
    Task<OrderCalculation> CalculateOrderAsync(Customer customer, OrderItem[] items);
    Task<string> ProcessPaymentAsync(decimal amount, PaymentDetails paymentDetails);
    Task SendNotificationsAsync(Customer customer, Order order);
}

public class OrderOperations : IOrderOperations
{
    private readonly OrderValidationService validationService;
    private readonly OrderCalculationService calculationService;
    private readonly OrderProcessingService processingService;
    
    public OrderOperations(
        OrderValidationService validationService,
        OrderCalculationService calculationService,
        OrderProcessingService processingService)
    {
        this.validationService = validationService;
        this.calculationService = calculationService;
        this.processingService = processingService;
    }
    
    public Task ValidateOrderAsync(CreateOrderRequest request) => 
        validationService.ValidateOrderAsync(request);
        
    public Task<OrderCalculation> CalculateOrderAsync(Customer customer, OrderItem[] items) => 
        calculationService.CalculateOrderAsync(customer, items);
        
    public Task<string> ProcessPaymentAsync(decimal amount, PaymentDetails paymentDetails) => 
        processingService.ProcessPaymentAsync(amount, paymentDetails);
        
    public Task SendNotificationsAsync(Customer customer, Order order) => 
        processingService.SendNotificationsAsync(customer, order);
}

// Even simpler OrderService using the aggregate
public class OrderService
{
    private readonly IOrderRepository orderRepository;
    private readonly ICustomerRepository customerRepository;
    private readonly IOrderOperations orderOperations;
    private readonly IUserContext userContext;
    
    // Only 4 dependencies!
    public OrderService(
        IOrderRepository orderRepository,
        ICustomerRepository customerRepository,
        IOrderOperations orderOperations,
        IUserContext userContext)
    {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
        this.orderOperations = orderOperations;
        this.userContext = userContext;
    }
    
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        var currentUser = userContext.GetCurrentUserId();
        var customer = await customerRepository.GetByIdAsync(request.CustomerId);
        
        await orderOperations.ValidateOrderAsync(request);
        
        var orderItems = CreateOrderItems(request.Items);
        var calculation = await orderOperations.CalculateOrderAsync(customer, orderItems);
        
        var order = new Order
        {
            CustomerId = customer.Id,
            Items = orderItems,
            Subtotal = calculation.Subtotal,
            Tax = calculation.Tax,
            Shipping = calculation.Shipping,
            Total = calculation.Total,
            CreatedBy = currentUser,
            CreatedAt = DateTime.UtcNow,
            PaymentId = await orderOperations.ProcessPaymentAsync(calculation.Total, request.PaymentDetails)
        };
        
        var savedOrder = await orderRepository.SaveAsync(order);
        await orderOperations.SendNotificationsAsync(customer, savedOrder);
        
        return savedOrder;
    }
}
            

DI Container Registration


public void ConfigureServices(IServiceCollection services)
{
    // Register the smaller, focused services
    services.AddScoped<OrderValidationService>();
    services.AddScoped<OrderCalculationService>();
    services.AddScoped<OrderProcessingService>();
    
    // Register the aggregate service
    services.AddScoped<IOrderOperations, OrderOperations>();
    
    // Register the main service
    services.AddScoped<OrderService>();
    
    // Register all the underlying dependencies
    services.AddScoped<IOrderRepository, SqlOrderRepository>();
    services.AddScoped<ICustomerRepository, SqlCustomerRepository>();
    services.AddScoped<IProductRepository, SqlProductRepository>();
    // ... other services
}
            

Easier Unit Testing

Testing becomes much more manageable with fewer dependencies:


[Test]
public async Task CreateOrderAsync_WithValidRequest_CreatesOrder()
{
    // Arrange - Only 4 mocks needed instead of 13!
    var mockOrderRepo = new Mock<IOrderRepository>();
    var mockCustomerRepo = new Mock<ICustomerRepository>();
    var mockOrderOperations = new Mock<IOrderOperations>();
    var mockUserContext = new Mock<IUserContext>();
    
    // Setup the mocks...
    var customer = new Customer { Id = 1, Email = "test@example.com" };
    mockCustomerRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(customer);
    mockUserContext.Setup(u => u.GetCurrentUserId()).Returns("user123");
    
    var calculation = new OrderCalculation { Subtotal = 100, Tax = 10, Shipping = 5, Total = 115 };
    mockOrderOperations.Setup(o => o.CalculateOrderAsync(customer, It.IsAny<OrderItem[]>()))
                      .ReturnsAsync(calculation);
    
    mockOrderOperations.Setup(o => o.ProcessPaymentAsync(115, It.IsAny<PaymentDetails>()))
                      .ReturnsAsync("payment123");
    
    var orderService = new OrderService(
        mockOrderRepo.Object,
        mockCustomerRepo.Object,
        mockOrderOperations.Object,
        mockUserContext.Object
    );
    
    var request = new CreateOrderRequest 
    { 
        CustomerId = 1, 
        Items = new[] { new CreateOrderItemRequest { ProductId = 1, Quantity = 2 } },
        PaymentDetails = new PaymentDetails()
    };
    
    // Act
    var result = await orderService.CreateOrderAsync(request);
    
    // Assert
    Assert.AreEqual(115, result.Total);
    Assert.AreEqual("payment123", result.PaymentId);
    mockOrderOperations.Verify(o => o.ValidateOrderAsync(request), Times.Once);
    mockOrderOperations.Verify(o => o.SendNotificationsAsync(customer, It.IsAny<Order>()), Times.Once);
}
            

Summary

The Constructor Over-injection anti-pattern often signals that a class is violating the Single Responsibility Principle by trying to do too much. While dependency injection encourages explicit dependencies, having too many dependencies makes code difficult to understand, test, and maintain.

The solution involves refactoring large classes into smaller, more focused services using techniques like extracting specialized services, creating aggregate services, or applying the Facade pattern. This approach reduces the number of constructor parameters, improves testability, and creates a more maintainable codebase. Remember: if your constructor has more than 3-4 parameters, consider whether your class is doing too much and needs to be broken down into smaller, more focused components.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Dependency Injection
  • Constructor Over-injection
  • Antipattern
  • DI
  • Too Many Dependencies
  • Code Smell