The Secret of Breath — Rediscover the sacred rhythm of your breath. Cultivate inner silence that brings clarity, balance, and resilience in daily life.


ASP.NET, Factory Patterns, and the Illusion of Control

The Desire to Control Creation

In the world of ASP.NET and .NET, there is a quiet but persistent instinct: the desire to control how things come into existence. Object creation, in particular, feels like something that must be carefully managed. If we explicitly construct our dependencies, we believe we understand the system better. If we centralize that construction, we believe we control it.

This instinct gives rise to factory patterns.

Factories promise order. Instead of scattering new across the codebase, we define structured pathways for object creation. We decide which implementation should exist under which condition. We encapsulate that decision behind a method, an interface, or a dedicated class.

At first glance, this feels like discipline. It feels like architecture.

But beneath that structure lies an important question: are we truly gaining control—or are we creating the illusion of it?

The Rise of Factories in .NET Codebases

Before dependency injection became idiomatic in modern .NET, factories played a central role in managing dependencies. Consider a familiar example:

public interface IPaymentService
{
    void Process(decimal amount);
}

public class StripePaymentService : 
IPaymentService
{
    public void Process(decimal amount)
    {
        // Stripe logic
    }
}

public class RazorpayPaymentService : 
IPaymentService
{
    public void Process(decimal amount)
    {
        // Razorpay logic
    }
}

public class PaymentServiceFactory
{
    public static IPaymentService 
    Create(string provider)
    {
        return provider switch
        {
            "Stripe" => new StripePaymentService(),
            "Razorpay" => new RazorpayPaymentService(),
            _ => throw new NotSupportedException()
        };
    }
}

Usage becomes straightforward:

var service = PaymentServiceFactory.
Create(provider);
service.Process(amount);

The calling code is clean. It does not know about concrete implementations. It simply asks for what it needs.

But something subtle has changed.

We are no longer looking at an action—we are looking at a request for an action. The line of code does not tell us what is being created; it tells us that something will be created elsewhere. To understand the system, we must now follow that indirection.

In trying to simplify usage, we have complicated understanding.

When Control Becomes Coupling

Factories are often introduced to reduce coupling. Ironically, they tend to centralize it.

Every new implementation must be registered in the factory. Every variation in behavior must be encoded into its logic. Over time, the factory evolves into a decision hub—a place where the system’s branching logic accumulates.

The calling code appears decoupled, but the system as a whole is not. The coupling has simply been relocated.

This introduces a cognitive gap. When reading code like:

var processor = _factory.Create(requestType);

we are no longer seeing what the system does. We are seeing a request whose outcome is hidden. To trace behavior, we must navigate through layers of abstraction.

At some point, the code stops expressing what is happening and begins expressing what we hope will happen elsewhere. That distance is where clarity begins to fade.

The Illusion of Flexibility

Factories are often justified as a way to make systems flexible. “We can swap implementations easily,” we say. “We can support multiple variations.”

But this flexibility is frequently conditional.

The factory must still know about every possible implementation. It must still encode every decision path. As the number of variations grows, so does the complexity of the factory. What began as a clean abstraction becomes a dense map of possibilities.

In some systems, this evolves into what can only be described as a hall of mirrors—factories creating factories, abstractions layered upon abstractions. The architecture becomes so “flexible” that it becomes difficult to navigate.

We gain the ability to support many scenarios, but lose the ability to clearly understand any single one.

ASP.NET Core and the Container as the Ultimate Factory

With ASP.NET Core, the approach to object creation shifted significantly. The built-in dependency injection container effectively acts as a large, implicit factory for the entire application.

When we write:

builder.Services.AddTransient<
IPaymentService, StripePaymentService>();

we are not creating objects—we are declaring relationships. We are defining rules that the container will use to construct objects when needed.

In this sense, the DI container is the ultimate factory. But unlike traditional factories, it distributes responsibility instead of centralizing it. Each component declares its dependencies, and the container orchestrates their creation.

This introduces a different model of control.

We no longer control how objects are created in a single place. Instead, we control how components relate to each other. The focus shifts from construction to composition.

There is still abstraction. There is still indirection. But it is structured in a way that reduces centralized decision-making.

A Useful Illusion: IHttpClientFactory

Not all illusions are harmful. Some are deeply beneficial.

Consider IHttpClientFactory in ASP.NET Core:

public class WeatherService
{
    private readonly IHttpClientFactory 
    _httpClientFactory;

    public WeatherService(IHttpClientFactory 
    httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<WeatherData> 
    GetWeatherAsync()
    {
        var client = _httpClientFactory.
        CreateClient("WeatherApi");
        return await client.GetFromJsonAsync
        <WeatherData>("forecast");
    }
}

Here, we appear to create a new HttpClient each time. In reality, the factory manages a pool of underlying handlers, optimizing resource usage and preventing issues like socket exhaustion.

This is an illusion—but a useful one.

It hides accidental complexity while preserving a simple mental model. We do not need to understand the intricacies of connection pooling or handler lifetimes. The abstraction earns its place by reducing real-world problems without introducing significant cognitive overhead.

This is the distinction that matters. Not all hidden complexity is harmful. The question is whether the abstraction removes friction—or merely relocates it.

The Trap of Over-Engineering

Problems arise when factories are introduced by default rather than by necessity.

In many .NET systems, factories are added preemptively—before real variability exists. The result is an architecture that anticipates change rather than responding to it. Layers are added “just in case,” and over time, these layers accumulate.

Sometimes, this leads to subtle runtime issues. For example, a singleton factory attempting to create services that depend on scoped lifetimes can introduce inconsistencies that are not immediately obvious. The abstraction hides the lifecycle mismatch until the system is under real load.

More often, however, the problem is simpler: the system becomes harder to read.

When a straightforward constructor injection would suffice, a factory introduces an unnecessary level of indirection. The cost is not performance—it is clarity.

When Factories Still Make Sense

Despite these pitfalls, factories are not inherently flawed. They remain valuable in specific scenarios.

When object creation depends on runtime data that cannot be resolved through dependency injection alone, factories provide a clean solution. When construction involves complex logic or domain-specific rules, encapsulating that logic in a factory can improve maintainability.

The key is restraint.

A factory should not be a registry of all possible implementations. It should serve a focused purpose. It should exist because the problem demands it—not because the pattern is available.

From Control to Clarity

The story of factory patterns in ASP.NET and .NET is ultimately a story about our relationship with control.

We are drawn to structures that make systems feel organized. We centralize decisions. We wrap complexity in abstractions. We build factories to manage the chaos of object creation.

But control, in software, is often an illusion.

We may control where decisions are made, but not necessarily how complex those decisions become. We may hide details, but hiding is not the same as simplifying.

The evolution toward dependency injection and framework-managed lifecycles reflects a deeper realization: clarity does not come from controlling every detail. It comes from designing systems where responsibilities are well-distributed and easy to follow.

Factories are not the enemy. But neither are they the solution to every design problem.

The goal is not to eliminate abstraction, nor to maximize it. The goal is to use it with awareness—to recognize when it clarifies, and when it obscures.

Because in the end, the most maintainable systems are not the ones we control the most.

They are the ones we understand the best.

That’s all for now. May your intention be clear and your mind be still. With this quiet wish, I rest my pen and return to the silence.


Author : Bipin Joshi
Bipin Joshi is an independent software consultant, trainer, and author, specializing in Microsoft web development technologies. Having embraced the yogic way of life, he also mentors select individuals in Ajapa Gayatri and allied meditative practices. Blending the disciplines of code and consciousness, he has been meditating, programming, writing, and teaching for over 31 years. As a prolific author, he shares his insights on both software development and yogic wisdom through his websites.

Posted On : 27 April 2026