Factories
A factory is an object that creates other objects.
Object Graph
flowchart TD
A[Caller / Coordinator] --> B[Context Factory]
B --> C["Context (per run)"]
A --> D["Pipeline Service (DI)"]
D --> E["Service1 Factory"]
E --> G["Service1 (per run)"]
F --> H["Service2 (per run)"]
G --> I["Result Object"]
D --> F["Service2 Factory"]
I --> H
C --> G
C --> H
H --> J["Result + Diagnostics"]What is it
A factory encapsulates construction logic so callers don’t need to know which concrete types to new or what dependencies to pass. You typically inject factories once, then call them whenever you need a new instance.
public interface IServiceFactory
{
IService Create(Context context);
}
public sealed class ServiceFactory : IServiceFactory
{
private readonly IStableDependency _stable;
public ServiceFactory(IStableDependency stable)
{
_stable = stable;
}
public IService Create(Context context)
{
return new Service(_stable, context);
}
}When to use
Best used when you have a workflow that creates short-lived objects per run, and you don’t want the coordinator to manually wire everything together.
It also helps when you expect multiple implementations (e.g., ServiceA vs ServiceB) selected at runtime.
When to use
- Object construction is non-trivial
- Need to build per-run instances safely
- When the creation logic could change (multiple implementations, feature flags, configuration, environment) - for example you want to DI some services in, but you still want to pass in some value into the constructor
When not to use
- Construction is trivial and unlikely to change
Basic structure
A common setup is: a coordinator injects factories, creates a context per run, then asks factories for the per-run services.
public interface IWorkflow
{
Result Run(string input);
}
public sealed class Workflow : IWorkflow
{
private readonly IContextFactory _contextFactory;
private readonly IServiceFactory _serviceFactory;
public Workflow(IContextFactory contextFactory, IServiceFactory serviceFactory)
{
_contextFactory = contextFactory;
_serviceFactory = serviceFactory;
}
public Result Run(string input)
{
var ctx = _contextFactory.Create(input);
var service = _serviceFactory.Create(ctx);
return service.Execute();
}
}Selecting between multiple implementations
Keep selection logic in one place so it doesn’t leak through the codebase. (Alternatively you can have a keyed service approach)
public enum ServiceKind { A, B }
public interface IServiceFactory
{
IService Create(ServiceKind kind, Context context);
}
public sealed class ServiceFactory : IServiceFactory
{
private readonly IStableDependency _stable;
public ServiceFactory(IStableDependency stable)
{
_stable = stable;
}
public IService Create(ServiceKind kind, Context context)
{
return kind switch
{
ServiceKind.A => new ServiceA(_stable, context),
ServiceKind.B => new ServiceB(_stable, context),
_ => throw new ArgumentOutOfRangeException(nameof(kind))
};
}
}Trade-offs
Pros
- Centralises construction logic and keeps coordinators small
- Supports multiple implementations cleanly
- Keeps per-run objects truly per-run, which avoids accidental shared state
- Works well with DI at the boundary without turning per-run data into global state
Cons
- Adds complexity
- Awkward to debug