Dependency Injection (DI) has long been praised as a fundamental component of code that is clear, testable, and manageable. Service containers (such as Spring in Java, the built-in container in ASP.NET Core, or well-known PHP containers like Symfony/PSR-11) are how most developers initially come into contact with it.
The problem is that service containers are only the first step. You're losing out on effective DI strategies that may advance the scalability, flexibility, and performance of your design if you settle for "bind this, resolve that."
Let's move beyond service containers in this post and examine sophisticated DI techniques that are used by seasoned developers.
Constructor Injection vs. Method Injection vs. Property Injection
The basic forms of DI are well-known, but choosing the right one can massively impact testability and coupling.
Constructor Injection (preferred default):
public class OrderService
{
private readonly IPaymentGateway _gateway;
public OrderService(IPaymentGateway gateway)
{
_gateway = gateway;
}
public void PlaceOrder(Order order) => _gateway.Charge(order.Amount);
}
- ✅ Immutable dependencies
- ✅ Easy to test
- ❌ Can lead to “constructor bloat” if you inject too many
Method Injection (better for context-specific needs):
public void ProcessRefund(Order order, IPaymentGateway gateway)
{
gateway.Refund(order.Amount);
}
- ✅ Great for one-off dependencies
- ❌ Harder to test at scale
Property Injection (use sparingly):
public ILogger Logger { get; set; }
- ✅ Good for optional dependencies
- ❌ Risky if properties aren’t initialized
Pro tip: Use constructor injection by default. Fall back to method/property injection only when you want flexibility or optional behaviors.
PHP is still highly relevant for building scalable, dynamic apps—see why PHP is a top choice for e-commerce development
Composition Root Pattern – The Real Powerhouse
One of the biggest mistakes with DI is scattering container.Resolve<T>()
calls throughout the codebase. That’s just a service locator anti-pattern in disguise.
Instead, define a composition root – a single place where all object graphs are composed.
Example in ASP.NET Core:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IPaymentGateway, StripePaymentGateway>();
}
Your application will now execute without ever contacting the container again. This maintains the purity and framework independence of your essential domain logic.
Typed Factories (Dynamic Injection at Runtime)
Sometimes you need to decide dependencies at runtime – but without hardcoding new calls. Enter typed factories.
Example: Strategy-based payment gateways
public interface IPaymentGatewayFactory
{
IPaymentGateway Create(string provider);
}
Implementation using DI container (example in Autofac):
builder.RegisterType<StripePaymentGateway>().Keyed<IPaymentGateway>("stripe");
builder.RegisterType<PayPalPaymentGateway>().Keyed<IPaymentGateway>("paypal");
builder.Register<Func<string, IPaymentGateway>>(c =>
{
var ctx = c.Resolve<IComponentContext>();
return key => ctx.ResolveKeyed<IPaymentGateway>(key);
});
Usage:
var gateway = factory("paypal");
gateway.Charge(500);
Now you can resolve implementations dynamically without scattering if/else
or switch
statements everywhere.
Scoped & Contextual Injection
Not all dependencies should live forever. For example, a DbContext or request-specific cache needs a scoped lifetime.
Learn more about PHP & It's Trending Frameworks
Scoped injection in ASP.NET Core:
services.AddScoped<IUnitOfWork, EfUnitOfWork>();
Contextual injection goes even deeper – injecting different implementations based on runtime context. For example, logging differently in Development
vs Production
.
services.AddScoped<ILogger>(sp =>
{
var env = sp.GetRequiredService<IHostEnvironment>();
return env.IsDevelopment() ? new ConsoleLogger() : new CloudLogger();
});
Interception & Decorators (Cross-Cutting Concerns)
Instead of bloating your services with logging, caching, or retry logic, you can use decorators.
Example: Adding retry logic around an external API call.
public class RetryPaymentGateway : IPaymentGateway
{
private readonly IPaymentGateway _inner;
public RetryPaymentGateway(IPaymentGateway inner)
{
_inner = inner;
}
public void Charge(decimal amount)
{
int retries = 3;
while (true)
{
try
{
_inner.Charge(amount);
break;
}
catch (Exception ex) when (retries-- > 0)
{
// Retry
}
}
}
}
To wrap current services, this may be registered in DI Containers such as Autofac or SimpleInjector
Cross-cutting issues are now openly introduced while your business rationale remains clear.
DI Without a Container (Pure DI)
Understanding when to avoid using a container altogether is the true art of expertise.
A straightforward manual composition root might be more streamlined for tiny programs or libraries:
var gateway = new StripePaymentGateway();
var service = new OrderService(gateway);
This avoids unnecessary abstraction overhead. Remember: containers are a tool, not a rule.
Final Thoughts
Dependency Injection is not just about wiring dependencies through a service container. At its core, it’s a design principle that enables:
- Cleaner architecture – decoupling business logic from infrastructure concerns.
- Better testability – injecting fakes, mocks, and stubs without hacking core code.
- Flexibility at scale – swapping implementations at runtime or per environment.
- Maintainability – centralizing object creation in a composition root instead of spreading new calls everywhere. The real maturity comes when you:
- Use constructor injection as your default, but lean on method/property injection where flexibility is needed.
- Centralize dependency wiring in a composition root.
- Apply factories, scoped lifetimes, and decorators for runtime dynamism and cross-cutting concerns.
- Recognize when a container is helpful vs. when pure DI is simpler.
In short: a service container is a tool; Dependency Injection is a mindset. The more you treat DI as an architectural principle instead of a framework trick, the more resilient, scalable, and test-friendly your systems will become.
Top comments (0)