Factory Pattern
By the end of this lesson
Centralise construction when creating an object is genuinely complex.
A factory is one place that knows how to build an object. Callers ask for a finished thing and stay out of the details.
Two situations justify one. Construction is genuinely complex: several lookups, a generated identifier, a calculation, a set of child objects. Or the concrete type is not known until run time, and something has to choose it. Outside those two, a constructor is the factory, and it is already written.
public sealed class InvoiceFactory(
IInvoiceNumbering numbering,
ITaxRegionLookup taxRegions,
IClock clock)
{
public async Task<Invoice> FromOrderAsync(Order order, PaymentTerms terms, CancellationToken token)
{
TaxRegion region = await taxRegions.ForCountryAsync(order.ShippingCountry, token);
string number = await numbering.NextAsync(clock.UtcNow.Year, token);
DateTime dueUtc = terms.DueDateFrom(clock.UtcNow);
var lines = order.Lines
.Select(line => new InvoiceLine(
line.Description,
line.Quantity,
line.UnitPrice,
region.RateFor(line.ProductCategory)))
.ToList();
return new Invoice(order.CustomerId, number, dueUtc, lines);
}
}- Four things have to be settled before an invoice exists: the tax region for the destination, the next number in this year's sequence, the due date implied by the payment terms, and one invoice line per order line with the right rate applied.
- None of that belongs in the Invoice constructor. If it were there, the entity would need a numbering service and a tax lookup injected into it, which puts infrastructure dependencies inside the domain — the thing the previous module spent two lessons avoiding.
- It does not belong in the calling handler either, because then each of the three places that creates an invoice would repeat it, and the third one would use last year's sequence.
- The Invoice constructor stays a plain assignment plus its own invariant checks. Assembling the arguments is a separate job, and this is where it lives.
// Program.cs — the container registers each builder against a key
builder.Services.AddKeyedScoped<IPaymentRequestBuilder, CardRequestBuilder>("card");
builder.Services.AddKeyedScoped<IPaymentRequestBuilder, DirectDebitRequestBuilder>("direct-debit");
builder.Services.AddKeyedScoped<IPaymentRequestBuilder, BankTransferRequestBuilder>("bank-transfer");
// The factory is thin, because the container is doing the resolving
public sealed class PaymentRequestFactory(IServiceProvider services)
{
public IPaymentRequestBuilder For(PaymentMethod method) =>
services.GetKeyedService<IPaymentRequestBuilder>(method.Key)
?? throw new NotSupportedException($"No payment builder registered for {method.Key}.");
}- The method a customer chose is data, arriving with the request, so the type cannot be selected at compile time. Something has to map a value to an implementation, and that mapping is what this factory is.
- Keyed registrations have been part of the built-in dependency injection container since .NET 8, so the lookup itself needs no code of yours.
- The throw matters. A payment method with no registered builder is a configuration mistake, and failing on the first request with a clear message beats returning null and producing a confusing error three frames away.
- An alternative with no container involvement: inject IEnumerable of the interface and index it by a property each implementation exposes, as the SOLID lesson did with discount policies. Choose on taste and on whether you mind the factory knowing about IServiceProvider.
Three shapes, in rough order of how often they are the right one:
- A named constructor on the type
- A static method such as Invoice.Draft or Employee.Joining that calls a private constructor. Use it when construction is a little involved but needs no outside dependencies. It costs nothing, keeps the rules next to the type, and gives the reader a name for the case.
- A dedicated factory class
- Use it when assembling the object needs dependencies the object itself should not have: a numbering service, a lookup, a clock. This is the case in the first example above.
- A resolving factory
- Use it when the concrete type depends on run-time data. Keyed services or a dictionary of implementations usually does the work; the factory is the one method that turns a value into an instance.
Summary
- A factory is justified by complex construction or by a type chosen at run time
- A named constructor covers the mild cases at no cost; a factory class covers cases needing dependencies
- Keyed registrations in the container handle run-time selection without hand-written plumbing
- If the body is a single new, the factory is redirection — delete it
- A factory that persists, or returns half-built objects, has taken on a job that belongs elsewhere
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Factory or constructor?
Decide for each of these whether it wants a factory, a named constructor, or nothing beyond the ordinary constructor: an Address built from five strings; an Employee record needing a generated staff number and a probation end date derived from a start date and a policy; a CsvExportOptions object with six settings that all have sensible defaults.
Show solution
Address: nothing. Five values in, one object out, no dependencies and no decisions. A constructor is exactly right, and a factory would only add a file.
Employee: a dedicated factory. It needs a numbering service for the staff number and a policy to derive the probation date, and neither belongs inside the entity. The entity's constructor still validates what it is given.
CsvExportOptions: nothing, or at most a named constructor for a common preset. With sensible defaults, object initialiser syntax or optional parameters read better than a factory, and the defaults are visible in one place.
The general test: does building the object require knowledge or dependencies that the object should not hold? If not, no factory.
Try it yourself
Make an invalid object impossible
Take an entity you maintain that can be constructed in an invalid state — properties with public setters that must all be filled in for the object to make sense.
Give it a private constructor and one or more named constructors for the valid starting states, then make the setters private. Note what breaks in your persistence layer.
Show solution
The gain is that invalid objects stop being expressible. Every route into the type goes through a method that names a real business case and checks its own rules, so a half-filled entity cannot reach a database.
What usually breaks first is your ORM's materialisation, which needs a way to construct entities when reading rows. EF Core can use a private parameterless constructor and set private properties through backing fields, so the fix is configuration rather than reopening the type.
Serialisation is the second thing to check. A type with no public setters may need a small amount of configuration to be deserialised, which is a good moment to ask whether the entity should be the thing crossing your API boundary at all — a separate request model is usually a better answer.
Saved in this browser only.