Many cooking techniques require heating oil in a pan. If you're unfamiliar with a recipe, you might preheat the oil before reviewing the instructions. By the time you've finished chopping vegetables, the oil is smoking. Novice cooks often mistake smoking oil for readiness, but smoke indicates the oil has exceeded its smoke point—breaking down, forming harmful compounds, and losing nutritional value.
In software design, patterns serve as recipes for solving recurring problems. When a solution appears correct but creates more problems than it solves, it becomes an anti-pattern.
Definition: An anti-pattern is a commonly adopted solution that produces negative consequences, despite documented alternatives that have proven effective.
Overheating oil exemplifies a culinary anti-pattern: it's a frequent mistake that seems logical but yields poor results. Similarly, DI anti-patterns represent recurring errors developers make when implementing dependency injection. This chapter examines four prevalent DI anti-patterns, all of which we've encountered in production codebases—and yes, we've committed them ourselves.
These anti-patterns often stem from genuine attempts to implement DI. However, incomplete adherence to DI principles transforms these efforts into harmful solutions. Recognizing these traps will help you navigate your first DI project and avoid subtle mistakes even experienced developers make.
Warning: Unlike other chapters, this one demonstrates how not to implement DI. Treat these examples as cautionary tales.
Each anti-pattern can be refactored into proper DI patterns from Chapter 4. The difficulty depends on implementation details. For each anti-pattern, we provide general guidance for refactoring toward better solutions.
Note: This chapter provides limited refactoring guidance due to space constraints. For comprehensive legacy code migration strategies, see Working Effectively with Legacy Code by Michael C. Feathers (Prentice Hall, 2004).
Legacy code sometimes requires aggressive measures to enable testing. Small, incremental steps prevent breaking existing functionality. While an anti-pattern might improve upon original code, it remains problematic—proven alternatives exist. Table 1 lists the anti-patterns covered.
| Anti-Pattern | Description |
|---|---|
| Control Freak | Direct control over dependency creation, opposite of Inversion of Control |
| Service Locator | Implicit service provider that may or may not supply dependencies |
| Ambient Context | Single dependency accessed through static members |
| Constrained Construction | Assumes specific constructor signatures for late binding |
Read sequentially or select sections of interest. If you read selectively, prioritize Control Freak and Service Locator.
As Constructor Injection is the most important DI pattern, Control Freak is the most common anti-pattern—it blocks proper DI entirely. Service Locator is most dangerous because it appears to solve problems. We'll address Service Locator after Control Freak.
Control Freak
What's the opposite of Inversion of Control? The term originally identified deviations from norms, but we need a concrete anti-pattern. Control Freak describes classes that refuse to relinquish control over volatile dependencies.
Definition: Control Freak appears whenever you directly control a volatile dependency outside the composition root, violating the Dependency Inversion Principle.
Using new to instantiate volatile dependencies demonstrates this anti-pattern. Listing 1 shows a typical implementation.
Listing 1: Control Freak example (problematic code)
public class DashboardController : Controller
{
public ViewResult Index()
{
var processor = new OrderProcessor(); // Controller directly creates volatile dependency
var orders = processor.GetPriorityOrders();
return this.View(orders);
}
}
Creating volatile dependencies explicitly declares lifetime control and prevents interception. While new becomes problematic for volatile dependencies, it's acceptable for stable ones.
Note: The
newkeyword isn't inherently evil, but avoid using it outside the composition root for volatile dependencies. Also beware static classes—they can be volatile dependencies too.
The most obvious Control Freak cases involve no abstraction attempts, as seen in Chapter 2's e-commerce examples. However, variants appear even when developers know about DI.
Example: Control Freak via Direct Dependency Initialization
Many developers hear "program to interfaces" without understanding the underlying principles. They write code that follows the rule but misses the point. Consider an OrderProcessor that uses an IOrderRepository abstraction:
public IEnumerable<discountedorder> GetPriorityOrders()
{
return
from order in this.repository.GetPriorityOrders()
select order.ApplyDiscountFor(this.userContext);
}
</discountedorder>
The repository field represents an abstraction. Chapter 3 showed proper Constructor Injection, but naive attempts exist. Listing 2 demonstrates one.
Listing 2: Direct repository initialization (problematic code)
private readonly IOrderRepository repository;
public OrderProcessor()
{
this.repository = new DatabaseOrderRepository(); // Control Freak in constructor
}
The field is declared as IOrderRepository, but runtime type is always DatabaseOrderRepository. Without code changes and recompilation, you cannot intercept or modify the repository. Hardcoding concrete types while defining abstractions provides little benefit.
Example: Control Freak Through Factory Variations
Developers often try factories to solve direct initialization problems. We'll examine three flawed approaches:
- Concrete Factory
- Abstract Factory
- Static Factory
Imagine developers discussing these options for IOrderRepository:
Developer A: We need an IOrderRepository in OrderProcessor, but it's an interface. The consultant said we shouldn't use new.
Devleoper B: What about a factory?
Developer A: I considered that, but I'm not sure how it helps. Look:
public class OrderRepositoryFactory
{
public IOrderRepository Create()
{
return new DatabaseOrderRepository();
}
}
Concrete Factory
Developer A: This factory encapsulates creation knowledge, but we still have to use it like this:
var factory = new OrderRepositoryFactory();
this.repository = factory.Create();
We've just moved the problem. OrderProcessor now controls the factory's lifetime, which controls the repository's lifetime. No interception opportunity exists.
Note: Concrete Factories aren't inherently bad—they solve other problems like code duplication. But they provide no DI value.
Abstract Factory
Developer B: Couldn't we abstract the factory?
public interface IOrderRepositoryFactory
{
IOrderRepository Create();
}
Developer A: But how do we get an instance of the factory?
Developer B: We'd create an implementation...
Developer A: ...which we'd have to instantiate in OrderProcessor, bringing us full circle.
Abstract Factory doesn't change their situation. They need an abstract IOrderRepositoryFactory instance, recreating the original problem.
Abstract Factories are overused: The pattern appears frequently, but class names often obscure this. Chapter 6 revisits why Abstract Factory is often a code smell in DI contexts.
Static Factory
Developer B: Let's make it static:
public static class OrderRepositoryFactory
{
public static IOrderRepository Create()
{
return new DatabaseOrderRepository();
}
}
Developer A: But we're still hardcoded to DatabaseOrderRepository.
Developer B: We'll use configuration:
public static IOrderRepository Create()
{
IConfigurationRoot config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
string repoType = config["orderRepository"];
switch (repoType)
{
case "sql": return new DatabaseOrderRepository();
case "azure": return new CloudOrderRepository();
default: throw new InvalidOperationException("...");
}
}
This static factory seems configurable but creates serious design problems. Figure 1 shows the dependency graph.
| Figure 1: Dependency graph for static factory solution |
|---|
The factory drags concrete implementations along as transitive dependencies, forcing OrderProcessor to implicitly depend on them. |
All classes reference IOrderRepository, but the factory depends on concrete implementations. Since OrderProcessor depends on the factory, it transitively depends on all implementations.
Real-world impact: This exact solution was proposed in a Fortune 500 project Mark consulted on. The recommendation was rejected due to existing codebase constraints. The project ultimately failed—not solely from DI issues, but as a symptom of deeper design problems.
Static factories cause circular dependencies between domain and data access layers. No matter where you place the factory, coupling occurs. The only way to avoid circular references is merging everything into one project, which eliminates modularity.
Worse, the factory drags along all implementations, even unused ones. Adding a third repository requires modifying and recompiling the factory. Testing becomes impossible without runtime configuration changes.
Example: Control Freak via Overloaded Constructors
Constructor overloading is common in .NET. Some overloads provide defaults while others accept full parameters. A DI anti-pattern emerges when test-specific overloads allow explicit dependency injection while production code uses parameterless constructors with foreign defaults.
Foreign Default: A volatile dependency's default implementation defined in a different module. Using
DatabaseOrderRepositoryas a default forces domain layer coupling to data access, dragging unwanted modules and complicating reuse.
Listing 3 shows OrderProcessor with problematic constructor overloads.
Listing 3: OrderProcessor with multiple constructors (problematic code)
private readonly IOrderRepository repository;
public OrderProcessor() : this(new DatabaseOrderRepository())
{
// Parameterless constructor forwards to overload with foreign default
}
public OrderProcessor(IOrderRepository repository)
{
if (repository == null)
throw new ArgumentNullException(nameof(repository));
this.repository = repository;
}
This approach seems convenient but imposes strong coupling. While OrderProcessor remains reusable, you lose interception capabilities in production code.
Control Freak Analysis
Control Freak is IoC's antithesis. Direct dependency creation produces tightly coupled code, sacrificing loose coupling benefits from Chapter 1.
It's the most common DI anti-pattern because new is the default object creation mechanism. Letting go of this control requires a mental shift many developers struggle with.
Negative Effects
- Limited substitution: You can't replace dependencies after compilation or provide specific instances.
- Reduced reusability: Modules drag undesirable dependencies into new contexts.
- Impaired parallel development: Tight coupling to implementations hinders independent work.
- Poor testability: Test doubles cannot replace dependencies.
While tightly coupled applications can be maintainable with great effort, the cost is unjustifiable when proper DI offers better alternatives.
Refactoring from Control Freak to DI
Refactor to Chapter 4's DI patterns, typically Constructor Injection:
- Program to abstractions (extract interfaces if needed).
- Consolidate dependency creation to a single method returning abstractions.
- Move creation out of the consuming class using Constructor Injection.
Listing 4 shows the refactored OrderProcessor.
Listing 4: Refactored OrderProcessor using Constructor Injection (good code)
public class OrderProcessor : IOrderProcessor
{
private readonly IOrderRepository repository;
public OrderProcessor(IOrderRepository repository)
{
if (repository == null)
throw new ArgumentNullException(nameof(repository));
this.repository = repository;
}
}
Control Freak is the most destructive anti-pattern. Even after addressing it, subtler problems remain. Next, we examine Service Locator.
Service Locator
Many developers elevate static factories to a more sophisticated level, creating the Service Locator anti-pattern.
Definition: Service Locator provides application components outside the composition root with access to an unbounded set of volatile dependencies.
Service Locator is pre-configured with concrete services, often in the composition root, via code or configuration files. Listing 5 demonstrates usage.
Listing 5: Service Locator anti-pattern example (problematic code)
public class DashboardController : Controller
{
public DashboardController() { }
public ViewResult Index()
{
IOrderProcessor processor =
ServiceRegistry.GetService<iorderprocessor>(); // Hidden dependency
var orders = processor.GetPriorityOrders();
return this.View(orders);
}
}
</iorderprocessor>
The parameterless constructor hides dependencies, making the class difficult to use and test. Figure 2 illustrates the relationship.
| Figure 2: Interaction between DashboardController and Service Locator |
|---|
| DashboardController requests an IOrderProcessor instance from the Service Locator, which returns a configured concrete implementation. |
Declaring Service Locator an anti-pattern was once controversial, but the debate is settled. You'll still encounter it in many codebases.
Our history with Service Locator: Mark maintained a Service Locator library for Enterprise Library 2 in 2007 before recognizing it as an anti-pattern. Steven created a similar library called "Simple Service Locator" in 2009, which evolved into Simple Injector after removing the anti-pattern.
DI containers resemble Service Locators structurally. The difference isn't implementation but usage: resolving complete object graphs in the composition root is correct; requesting fine-grained services elsewhere is Service Locator.
Important: A DI container encapsulated in the composition root is infrastructure, not a Service Locator.
Example: OrderProcessor Using Service Locator
Let's revisit OrderProcessor, which needs an IOrderRepository. Using Service Locator, it would call a static method:
Listing 6: Service Locator in constructor (problematic code)
public class OrderProcessor : IOrderProcessor
{
private readonly IOrderRepository repository;
public OrderProcessor()
{
this.repository = ServiceRegistry.GetService<iorderrepository>();
}
public IEnumerable<discountedorder> GetPriorityOrders() { ... }
}
</discountedorder></iorderrepository>
Listing 7 shows a minimal Service Locator implementation.
Listing 7: Simple Service Locator implementation
public static class ServiceRegistry
{
private static readonly Dictionary<type object=""> services =
new Dictionary<type object="">();
public static void Register<t>(T service)
{
services[typeof(T)] = service;
}
public static T GetService<t>()
{
return (T)services[typeof(T)];
}
public static void Reset()
{
services.Clear();
}
}
</t></t></type></type>
Clients call GetService<t></t> to request instances. The dictionary stores mappings. Without guard clauses, missing types throw KeyNotFoundException. The Reset method enables test isolation.
Listing 8 shows a unit test depending on Service Locator.
Listing 8: Unit test depending on Service Locator (problematic code)
[Fact]
public void GetPriorityOrders_ReturnsResults()
{
// Arrange
var stub = new OrderRepositoryStub();
ServiceRegistry.Reset();
ServiceRegistry.Register<iorderrepository>(stub);
var sut = new OrderProcessor();
// Act
var result = sut.GetPriorityOrders(); // Temporal coupling between Register and method call
// Assert
Assert.NotNull(result);
}
</iorderrepository>
The test configures a stub before creating the system under test. If registration is forgotten, runtime errors occur.
Service Locator Analysis
Service Locator is dangerous because it almost works. It enables dependency replacement and testing. Evaluating it against modular design principles shows it meets most criteria:
- Late binding support through configuration changes
- Parallel development via interface-based programming
- Separation of concerns potential
- Testability via test doubles
It fails in one critical area: component reusability.
Negative Effects
Service Locator impacts reusability in two ways:
- Drags redundant dependency: Components depend on both their required abstractions and the Service Locator itself. Reusing a component requires redistributing the Service Locator module.
- Hides dependencies: The public API doesn't reveal required dependencies. Visual Studio's IntelliSense only shows a parameterless constructor, masking complexity. Future versions might add dependencies, causing breaking changes without compile-time warnings.
Figure 3 shows OrderProcessor's dependency graph.
| Figure 3: OrderProcessor dependency graph |
|---|
| OrderProcessor depends on both IOrderRepository and ServiceRegistry, creating a redundant dependency. |
Generic signatures like public T Resolve<t>()</t> appear strongly typed but are weak—any type can be requested, with no compile-time guarantees.
Unit tests suffer from interdependent tests when registered test doubles persist in memory, requiring explicit teardown via Reset().
Refactoring from Service Locator to DI
Constructor Injection statically declares dependencies, enabling compile-time validation with Pure DI or startup-time validation with containers.
The refactoring process:
- Consolidate dependency creation into a single method.
- Introduce a readonly field to hold the dependency.
- Replace Service Locator calls with constructor parameter assignment.
Start with top-level classes and work down the dependency graph to minimize breaking changes.
Service Locator looks correct but sacrifices crucial design qualities. Chapter 4's patterns offer superior alternatives.
Ambient Context
Related to Service Locator, Ambient Context provides global access to a single, strongly typed dependency through static members.
Definition: Ambient Context provides global access to a volatile dependency or its behavior via static class members from outside the composition root.
Listing 9 demonstrates this pattern.
Listing 9: Ambient Context example (problematic code)
public string GenerateGreeting()
{
IClock clock = SystemClock.Current; // Hidden dependency via static accessor
DateTime now = clock.Now;
string period = now.Hour < 6 ? "night" : "day";
return $"Good {period}.";
}
IClock abstracts time access. Hiding DateTime.Now behind an abstraction is good, but providing static access is problematic.
Ambient Context resembles Singleton but allows dependency replacement. Singleton is acceptable only in composition roots or with stable dependencies.
Note: Misused Singleton for volatile dependencies has the same effect as Ambient Context.
Example: Time Access via Ambient Context
Controlling time is crucial for testing time-dependent logic. Listing 10 shows the abstraction.
Listing 10: IClock abstraction
public interface IClock
{
DateTime Now { get; }
}
Listing 11 shows a problematic Ambient Context implementation.
Listing 11: SystemClock Ambient Context (problematic code)
public static class SystemClock // Static class providing global access
{
private static IClock current = new DefaultClock(); // Local default
public static IClock Current // Global read/write property
{
get => current;
set => current = value ?? throw new ArgumentNullException(nameof(value));
}
private class DefaultClock : IClock // Uses system clock
{
public DateTime Now => DateTime.Now;
}
}
Listing 12 shows a unit test depending on Ambient Context.
Listing 12: Unit test depending on Ambient Context (problematic code)
[Fact]
public void Greeting_DuringDaytime_ReturnsDay()
{
// Arrange
DateTime daytime = DateTime.Parse("2019-01-01 6:00");
var stub = new ClockStub { Now = daytime };
SystemClock.Current = stub; // Replaces global state
var generator = new GreetingGenerator(); // Constructor hides dependency
// Act
string message = generator.GenerateGreeting(); // Temporal coupling
// Assert
Assert.Equal("Good day.", message);
}
Common variations include:
- Ambient Context providing static methods that hide dependency usage
- Combined abstract class with static and instance members
- Delegate-based Ambient Context using
Func<datetime></datetime>
Example: Logging via Ambient Context
Logging is another common Ambient Context trap. Developers justify it because "every class needs logging." Listing 13 shows typical code.
Listing 13: Logging Ambient Context (problematic code)
public class NotificationService
{
private static readonly ILogger Logger =
LogManager.GetLogger(typeof(NotificationService)); // Hidden dependency
public void SendNotification()
{
Logger.Info("Sending notification"); // Uses static field
// ...
}
}
Developers copy such patterns from library documentation, which prioritizes simplicity over best practices. The "every class needs logging" mindset leads to Constructor Over-injection (covered in Chapter 6).
Note: We're not anti-logging—it's critical. But design your system so only a few components handle logging, not the entire codebase. Chapter 10 demonstrates how.
Ambient Context Analysis
Developers use Ambient Context for cross-cutting concerns they consider "universal," justifying departure from Constructor Injection.
Negative Effects
- Hidden dependencies: Mask classes with too many dependencies, violating Single Responsibility Principle.
- Testing complexity: Global state causes interdependent tests requiring teardown logic.
- Context-specific behavior: Difficult to provide different implementations for different consumers.
- Temporal coupling: Initialization and usage are separated, preventing fail-fast behavior.
Real-world consequence: Steven once worked on a codebase using Common.Logging's Ambient Context. A bug caused application startup failures on some developer machines. The organization wasted countless hours troubleshooting. Proper DI would have allowed local replacement in the composition root within minutes.
Refactoring from Ambient Context to DI
Refactoring is straightforward when DI is already established:
- Centralize Ambient Context calls in the constructor.
- Create a private readonly field assigned from the constructor.
- Replace static calls with the field reference.
- Add a constructor parameter with guard clauses.
Listing 14 shows the refactored result.
Listing 14: Refactored to Constructor Injection (good code)
public class GreetingGenerator
{
private readonly IClock clock;
public GreetingGenerator(IClock clock)
{
this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
}
public string GenerateGreeting()
{
DateTime now = this.clock.Now;
// ...
}
}
For codebases without DI, address Control Freak and Service Locator first.
Ambient Context seems convenient for cross-cutting concerns but often masks larger design issues. Chapter 4's patterns and Chapter 10's design techniques provide better solutions.
Constrained Construction
After moving all dependency creation to a composition root, another pitfall emerges: requiring specific constructor signatures for late binding.
Definition: Constrained Construction forces all implementations of an abstraction to share a constructor signature, enabling late binding.
This only applies when you need late binding. With early binding (direct references), no constraints exist—but you lose compile-time replaceability.
Listing 15 shows the anti-pattern.
Listing 15: Constrained Construction example (problematic code)
public class DatabaseOrderRepository : IOrderRepository
{
public DatabaseOrderRepository(string connectionString)
{
// ...
}
}
public class CloudOrderRepository : IOrderRepository
{
public CloudOrderRepository(string connectionString)
{
// ...
}
}
All IOrderRepository implementations must have identical constructor signatures. This limits flexibility, as discussed in section 1.2.2.
Example: Late Binding OrderRepository
In an e-commerce application, classes depend on IOrderRepository. The composition root (e.g., ASP.NET Core startup) should create implementations. Listing 16 shows problematic late binding.
Listing 16: Implicit constructor constraint (problematic code)
string connectionString = Configuration.GetConnectionString("CommerceDb");
string repoTypeName = Configuration["OrderRepositoryType"];
Type repositoryType = Type.GetType(repoTypeName, throwOnError: true);
var constructorArgs = new object[] { connectionString };
IOrderRepository repository = (IOrderRepository)Activator.CreateInstance(
repositoryType, constructorArgs); // Requires specific signature
Configuration file:
{
"ConnectionStrings": {
"CommerceDb": "Server=.;Database=Commerce;Trusted_Connection=True;"
},
"OrderRepositoryType": "DatabaseOrderRepository, Commerce.Data"
}
The connection string in configuration should raise suspicion—why would an abstraction need database connection details? The implicit constraint requires all implementations to accept a single string parameter, which makes no sense for REST-based or file-based repositories.
Tip: Model dependency construction only through explicit constraints (interfaces or base classes).
Constrained Construction Analysis
A more common constraint requires parameterless constructors for simple Activator.CreateInstance calls:
IOrderRepository repository = (IOrderRepository)Activator.CreateInstance(repositoryType);
This lowest common denominator severely restricts flexibility.
Negative Effects
- Lost flexibility: Cannot share instances across multiple consumers (Figure 4).
- Increased memory usage: Multiple instances instead of shared ones.
- Lifetime management issues: Difficult to manage object lifetimes.
| Figure 4: Sharing a single DataContext instance between repositories |
|---|
| You want one CommerceContext instance injected into multiple repositories, which requires external injection. |
DI enables instance sharing, but this relates to lifetime management (Chapter 8).
Refactoring from Constrained Construction to DI
When you need late binding without constructor constraints, Abstract Factory seems tempting but introduces complexity. Consider an IOrderRepositoryFactory (Figure 5).
| Figure 5: Attempting Abstract Factory to solve late binding |
|---|
| IOrderRepository is the real dependency, but IOrderRepositoryFactory attempts to solve the late binding challenge. |
What if DatabaseOrderRepository needs an IUserContext?
Listing 17: DatabaseOrderRepository requiring IUserContext
public class DatabaseOrderRepository : IOrderRepository
{
public DatabaseOrderRepository(IUserContext userContext, DataContext dbContext)
{
// ...
}
}
The factory must compose this object graph. To prevent coupling the data access library to UI components, the factory must reside in a separate assembly (Figure 6).
| Figure 6: Factory dependencies in separate assembly |
|---|
| The factory must reference all dependencies, requiring implementation in a separate assembly to avoid circular references. |
Listing 18: Factory creating DatabaseOrderRepository (problematic code)
public class DatabaseOrderRepositoryFactory : IOrderRepositoryFactory
{
private readonly string connectionString;
public DatabaseOrderRepositoryFactory(IConfigurationRoot configuration)
{
this.connectionString = configuration.GetConnectionString("CommerceDb");
}
public IOrderRepository Create()
{
return new DatabaseOrderRepository(
new WebUserContextAdapter(), // From different assembly
new DataContext(connectionString));
}
}
This Abstract Composition Root directly depends on concrete types, limiting flexibility. If the core application wants to replace IUserContext, both the core and factory projects must change.
Tip: A general-purpose DI container prevents Constrained Construction by using reflection to analyze constructors. Part 4 covers containers in detail.
Abstract Composition Root is only necessary when you must plug in new assemblies without recompiling existing code. Most applications don't need this flexibility. Early binding with a centralized composition root is simpler and cheaper to maintain.
Note: Constrained Construction only applies to late binding. Early binding avoids implicit constraints through compiler enforcement.
DI is a set of patterns without mechanical validation tools. Chapter 4 covered proper patterns; this chapter examined common failures. Learning from others' mistakes helps you avoid them.
The anti-patterns discussed—Control Freak, Service Locator, Ambient Context, and Constrained Construction—all appear in real codebases. Recognizing them is the first step. Chapter 6 will address challenges that seem to resist simple solutions.