Every object in your application is born somewhere. Most of the time it's a simple new:
var service = new OrderService(); And most of the time that's perfectly fine. But when the class you create has dependencies, when you need to decide at runtime which class to create, or when creating it is expensive, that innocent new becomes a problem: it ties your code to a concrete type and spreads creation logic everywhere.
This is exactly the kind of problem creational patterns solve. They help you with three things:
- Controlling the complexity of object creation;
- Managing object lifecycles;
- Providing alternatives to direct instantiation when it becomes a problem.
The family at a glance
| Pattern | What it does | Use it when |
|---|---|---|
| Singleton (GoF) | Ensures a class has only one instance, with global access | One shared instance for the whole app (in .NET, usually a DI lifetime) |
| Factory Method (GoF) | Lets subclasses decide which class to create | The creation step varies by subclass |
| Simple Factory | Puts the creation decision in one class or method | The choice depends on runtime data |
| Abstract Factory (GoF) | Creates families of related objects | Objects must always match each other |
| Builder (GoF) | Builds complex objects step by step | Many optional parameters, or a fluent API |
| Prototype (GoF) | Creates new objects by cloning existing ones | Copying is cheaper or simpler than building |
| Object Pool | Reuses expensive objects instead of recreating them | Creation is costly and the objects are short-lived |
| Lazy Initialization | Defers creation until the object is needed | The object is expensive and may never be used |
| Dependency Injection | Gives objects their dependencies from outside | Almost always, in modern .NET |
| Service Locator | A central registry to look services up | Rarely: it hides dependencies, so it's often an anti-pattern |
What modern .NET changes
The intent of these patterns hasn't changed since 1994. What changed is how much code you need to write for them.
The DI container does a lot of the work. The classic Singleton, with its locks and thread-safety concerns, is often just a lifetime today:
builder.Services.AddSingleton<IPriceCalculator, PriceCalculator>(); The container creates the instance once, shares it, and disposes it at shutdown. Scoped, transient and singleton lifetimes are first-class concepts, handled the same way across the whole application.
The language helps too. Records and init-only properties give you immutable objects without the boilerplate, and the with expression is a built-in, shallow Prototype:
public record Customer(string FirstName, string LastName, int Age); var customer = new Customer("Mario", "Rossi", 40); var olderCustomer = customer with { Age = 41 }; // a copy, the original is untouched Switch expressions make simple factories short and explicit:
public static INotification Create(NotificationChannel channel) => channel switch { NotificationChannel.Email => new EmailNotification(), NotificationChannel.Sms => new SmsNotification(), _ => throw new NotSupportedException($"No notification for {channel}") }; Performance is part of the picture. The garbage collector is very good at short-lived objects, so pooling is not a default anymore. But for large buffers and expensive resources, ArrayPool<T>, ObjectPool<T> and connection pools are still the right tool.
A few rules that hold for all of them
- Start with
newand the DI container. Reach for a pattern when a real problem shows up, not before. - Keep creation in one place. Whether it's a factory, a builder or the composition root, scattered
newcalls are the real smell. - Prefer immutable objects. They make many creational problems, thread safety first of all, disappear.
- Don't let the container leak. Injecting
IServiceProvidereverywhere turns Dependency Injection back into a Service Locator.
Want the full story?
This is the short version. The complete article on Level Up Coding goes through how object creation evolved with the language and the platform, from classic OOP to records, pattern matching and the built-in DI container. It also covers performance and the garbage collector, immutability and thread safety, cloud-native constraints, generics and static abstract members, testing, and the trends that are shaping the next years.
Read it here: Creational Patterns in .NET C#: Building objects in the modern Era.
Each pattern then has its own deep dive, starting with the Singleton.
Thanks for reading.