Design patterns come up constantly in technical interviews, and get explained inconsistently almost everywhere - either as abstract UML diagrams with no real code, or as a wall of all twenty-three GoF (Gang Of Four) patterns covered so shallowly that none of them actually stick.
This is the first post in a three-part series covering the patterns that genuinely come up in interviews and in production code, organized by category, with a consistent format for each: the problem it solves, an analogy, a before-and-after code example, and honest guidance on when it's actually worth the added complexity, not just when it exists as an option.
What a Design Pattern Actually Is
A design pattern is a reusable, named solution to a problem that shows up repeatedly in object-oriented code. It is not a library, not a specific piece of code to copy and paste, and not tied to any single language - it is a shape of solution, described generally enough to apply across many different contexts. Knowing the name of a pattern is genuinely useful in a team setting, because saying "let's use a Factory here" communicates an entire design decision in three words to anyone who already knows what that means.

Design Patterns in C#, Part 1 of 3 — Creational patterns first, Structural and Behavioral to follow. The patterns that actually come up in interviews, not all 23.
The Three Categories
Design patterns traditionally split into three categories. Creational patterns are concerned with how objects get created - Singleton, Factory Method, Abstract Factory, Builder, and Prototype all fall here. Structural patterns are concerned with how objects and classes are composed into larger structures - Adapter, Decorator, Facade, Composite, Proxy, and Bridge fall here. Behavioral patterns are concerned with how objects communicate and share responsibility - Strategy, Observer, Command, State, Template Method, Iterator, and Chain of Responsibility fall here.
This post covers Creational patterns specifically - the four that come up most often in real interviews and real code: Singleton, Factory Method, Builder, and Abstract Factory.

Four creational patterns, four different answers to the same question — how should this object actually get created?
Pattern 1: Singleton
The problem Singleton solves is ensuring a class has exactly one instance throughout the application's lifetime, with a single globally accessible point to reach it - typically for something genuinely singular by nature, like application-wide configuration or a shared connection pool.
Think of a country's government. There is exactly one at a time, and any department that needs to interact with "the government" reaches the same one - there is no mechanism for a second, competing government to exist alongside it. Singleton enforces that same one-and-only-one guarantee in code.
// WITHOUT Singleton - nothing stops multiple instances
public class AppConfiguration
{
public string ConnectionString { get; set; }
public AppConfiguration()
{
// expensive loading from file/environment
}
}
// Nothing prevents this - each call creates a
// separate instance, each doing the expensive load again
var config1 = new AppConfiguration();
var config2 = new AppConfiguration();
// config1 and config2 are NOT the same object
// WITH Singleton - exactly one instance, ever
public sealed class AppConfiguration
{
private static readonly Lazy<AppConfiguration> _instance =
new(() => new AppConfiguration());
public static AppConfiguration Instance => _instance.Value;
public string ConnectionString { get; private set; }
// private constructor - nobody outside this class
// can call "new AppConfiguration()"
private AppConfiguration()
{
ConnectionString = LoadFromEnvironment();
}
private string LoadFromEnvironment()
=> Environment.GetEnvironmentVariable("DB_CONNECTION")
?? "default-connection-string";
}
// Every call anywhere in the app gets the SAME instance
var config = AppConfiguration.Instance;
var sameConfig = AppConfiguration.Instance;
// config and sameConfig ARE the same object
// Lazy<T> makes this thread-safe automatically - two
// threads calling Instance simultaneously will still
// only ever construct ONE AppConfiguration
Singleton is more controversial than most tutorials present it as being. It introduces global state, which makes unit testing genuinely harder - a test that depends on Singleton state can leak that state into the next test, causing flaky, order-dependent failures. In modern C#, dependency injection with a singleton lifetime registration in ASP.NET Core's built-in container achieves the same one-instance guarantee without the static-access downsides - the instance is still created once, but it's injected rather than reached through a static property, which keeps code testable. Reach for the classic Singleton pattern sparingly, and prefer DI-managed singleton lifetime instead whenever a framework already offers it.
Pattern 2: Factory Method
The problem Factory Method solves is creating objects without specifying the exact concrete class in the calling code - letting a subclass, or a parameter, decide which concrete type actually gets instantiated.
Think of a restaurant kitchen. A customer doesn't need to know how a dish is prepared - they order "pasta," and the kitchen decides internally which specific process makes that pasta, while the ordering code never touches the specific preparation steps. Factory Method works the same way: calling code asks for a type of thing, and a factory method decides which concrete class actually fulfills that request.
// WITHOUT Factory Method - calling code is tightly
// coupled to every concrete notification type
public class NotificationSender
{
public void Send(string type, string message)
{
if (type == "Email")
{
var email = new EmailNotification();
email.Send(message);
}
else if (type == "SMS")
{
var sms = new SmsNotification();
sms.Send(message);
}
// every new notification type means editing
// this method - an Open/Closed Principle
// violation as a bonus problem
}
}
// WITH Factory Method
public abstract class Notification
{
public abstract void Send(string message);
}
public class EmailNotification : Notification
{
public override void Send(string message)
=> Console.WriteLine($"Email: {message}");
}
public class SmsNotification : Notification
{
public override void Send(string message)
=> Console.WriteLine($"SMS: {message}");
}
// The factory method - the one place that knows
// how to create each concrete type
public abstract class NotificationCreator
{
public abstract Notification CreateNotification();
// Template-style usage of the factory method
public void Notify(string message)
{
var notification = CreateNotification();
notification.Send(message);
}
}
public class EmailNotificationCreator : NotificationCreator
{
public override Notification CreateNotification()
=> new EmailNotification();
}
public class SmsNotificationCreator : NotificationCreator
{
public override Notification CreateNotification()
=> new SmsNotification();
}
// Calling code depends only on the abstraction
NotificationCreator creator = new EmailNotificationCreator();
creator.Notify("Your order has shipped");
// Adding a new notification type = one new class.
// Nothing that already works needs to change.
Factory Method earns its place once there are genuinely multiple concrete types that share a common interface, and the decision of which one to create needs to stay flexible or extensible. For a fixed, small, unchanging set of two types that will realistically never grow, a simple if/else or switch statement can actually be more readable - the same tradeoff covered in an earlier Open/Closed Principle post applies here: the pattern pays off once new cases keep appearing, not automatically from day one.
Pattern 3: Builder
The problem Builder solves is constructing a complex object step by step, particularly one with many optional parameters, without resorting to a constructor with an unreadable number of positional arguments.
Think of ordering a custom sandwich at a deli. Nobody hands the person behind the counter a single sentence listing every possible ingredient in a fixed order, half of them "none." Instead, each choice gets specified step by step - bread, then protein, then toppings, then whether to toast it - and the sandwich comes together incrementally. Builder applies that same step-by-step, readable construction to objects with a lot of optional configuration.
// WITHOUT Builder - the classic "telescoping
// constructor" problem
public class Report
{
public Report(string title, string subtitle,
string author, bool includeCharts,
string footer, bool includePageNumbers,
string watermark, string format)
{
// ... assign all eight fields
}
}
// What does "true, null, false" mean here?
// Nobody can tell without checking the constructor
var report = new Report(
"Q4 Report", null, null, true,
null, false, null, "PDF"
);
// WITH Builder - readable at the call site
public class Report
{
public string Title { get; set; }
public string Subtitle { get; set; }
public string Author { get; set; }
public bool IncludeCharts { get; set; }
public string Format { get; set; }
}
public class ReportBuilder
{
private readonly Report _report = new();
public ReportBuilder WithTitle(string title)
{
_report.Title = title;
return this;
}
public ReportBuilder WithSubtitle(string subtitle)
{
_report.Subtitle = subtitle;
return this;
}
public ReportBuilder WithAuthor(string author)
{
_report.Author = author;
return this;
}
public ReportBuilder IncludeCharts()
{
_report.IncludeCharts = true;
return this;
}
public ReportBuilder AsFormat(string format)
{
_report.Format = format;
return this;
}
public Report Build() => _report;
}
// Every line self-documents what is being set -
// no positional guessing required
var report = new ReportBuilder()
.WithTitle("Q4 Report")
.WithAuthor("Manohari")
.IncludeCharts()
.AsFormat("PDF")
.Build();
Builder earns its place once an object has enough optional fields that a constructor becomes genuinely hard to read at the call site - a rough rule of thumb is four or more optional parameters. C#'s object initializer syntax already solves much of this same problem natively for simple cases, so Builder's real value shows up when construction needs actual step-by-step logic, validation between steps, or a fluent API that guides the caller through a specific required sequence - not just as a fancier way to set properties that object initializers already handle well.
Pattern 4: Abstract Factory
The problem Abstract Factory solves is creating families of related objects that need to stay consistent with each other, without the calling code specifying their concrete classes directly.
Think of furnishing a room in a specific style - Victorian or Modern. Choosing "Victorian" should consistently produce a Victorian chair, a Victorian table, and a Victorian lamp together, not a Victorian chair paired with a Modern lamp by accident. Abstract Factory guarantees that an entire family of related objects stays consistent, because one factory is responsible for the whole family rather than each piece being created independently.
// The abstract product interfaces
public interface IButton
{
void Render();
}
public interface ICheckbox
{
void Render();
}
// Concrete products - Windows family
public class WindowsButton : IButton
{
public void Render() => Console.WriteLine("Windows button");
}
public class WindowsCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Windows checkbox");
}
// Concrete products - macOS family
public class MacButton : IButton
{
public void Render() => Console.WriteLine("Mac button");
}
public class MacCheckbox : ICheckbox
{
public void Render() => Console.WriteLine("Mac checkbox");
}
// The abstract factory - produces a FAMILY of
// related products
public interface IUiFactory
{
IButton CreateButton();
ICheckbox CreateCheckbox();
}
public class WindowsUiFactory : IUiFactory
{
public IButton CreateButton() => new WindowsButton();
public ICheckbox CreateCheckbox() => new WindowsCheckbox();
}
public class MacUiFactory : IUiFactory
{
public IButton CreateButton() => new MacButton();
public ICheckbox CreateCheckbox() => new MacCheckbox();
}
// Calling code depends only on IUiFactory - it never
// touches WindowsButton or MacButton directly, and it
// is IMPOSSIBLE to accidentally mix a Windows button
// with a Mac checkbox
public class Application
{
private readonly IUiFactory _factory;
public Application(IUiFactory factory)
=> _factory = factory;
public void RenderUi()
{
var button = _factory.CreateButton();
var checkbox = _factory.CreateCheckbox();
button.Render();
checkbox.Render();
}
}
// Swapping the entire family is one line
var app = new Application(new WindowsUiFactory());
app.RenderUi();
// "Windows button" and "Windows checkbox" - guaranteed
// to always be a matched pair
Abstract Factory is worth the added complexity specifically when there are genuinely multiple related products that must be created together consistently, and multiple families of that same set exist - Windows versus Mac UI components, or different cloud provider SDKs behind a common interface, are realistic examples. For a single product with no "family" concept at all, that's Factory Method's job instead - reaching for Abstract Factory when Factory Method would do is a common source of unnecessary complexity in real codebases.
Factory Method vs Abstract Factory
This is the distinction that trips people up most often. Factory Method creates one product, and subclassing decides which concrete type gets created. Abstract Factory creates a family of related products, and composition - injecting a specific factory - decides which family, while guaranteeing the products within that family stay consistent with each other.
A useful test for telling them apart: if adding a new "thing" means adding one new method to create it, that's Factory Method territory. If adding a new "thing" means several related objects need to be created together and stay consistent, that's Abstract Factory territory.
Key Lessons
Singleton guarantees exactly one instance, but introduces global state that makes unit testing harder - prefer DI-managed singleton lifetime over the classic static-access pattern wherever a framework already offers it.
Factory Method decouples calling code from concrete types, and pays off once new types keep getting added - a fixed, small, unchanging set may not need it at all.
Builder solves the telescoping constructor problem for objects with many optional parameters, though object initializer syntax already covers simpler cases natively in C#.
Abstract Factory guarantees a family of related objects stays consistent with each other, and is worth reaching for only when there genuinely are multiple related products and multiple families - not for a single product with no family concept.
Every pattern covered here has a real cost in added complexity and indirection, and that cost should be weighed against the actual problem being solved rather than applied by default just because the pattern exists.
What's Next
Part 2 of this series covers Structural patterns - Adapter, Decorator, Facade, and Composite - the patterns concerned with how objects and classes combine into larger structures. Part 3 covers Behavioral patterns - Strategy, Observer, Command, State, and Template Method - the patterns concerned with how objects communicate and share responsibility with each other.
Summary
Creational patterns solve one consistent underlying question - how should an object actually get created - in four different ways depending on the specific shape of the problem. Singleton guarantees exactly one instance. Factory Method lets a subclass decide the concrete type. Builder makes complex, optional-heavy construction readable. Abstract Factory guarantees an entire family of related objects stays consistent. None of these patterns is free - each adds a layer of indirection that only pays for itself once the problem it solves is actually present in the code, not preemptively.
Originally published at TechStack Blog: https://www.techstackblog.com/post.html?slug=design-patterns-creational-csharp
More from TechStack Blog: CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
Top comments (0)