Design Patterns in C#
In C#, design patterns are reusable, proven approaches to solving common software-design and architecture problems. They are not libraries, frameworks, or fixed code templates. Instead, they provide guidelines for organizing classes, objects, and responsibilities so that applications become maintainable, scalable, flexible, testable, and easier to extend.
Design patterns are especially useful in enterprise applications built using C#, .NET, ASP.NET Core, Web API, Entity Framework Core, Angular, and microservices.
Why Use Design Patterns?
Design patterns help developers:
Reduce code duplication.
Improve code maintainability.
Follow SOLID principles.
Reduce tight coupling between components.
Improve code reusability.
Make applications easier to test.
Make new features easier to add.
Provide a common vocabulary between developers.
Improve separation of concerns.
Make complex applications easier to understand.
Support scalable and extensible architectures.
Reduce the risk of introducing breaking changes.
For example, instead of creating a large class that handles database operations, business logic, validation, logging, and notifications, design patterns help separate these responsibilities into appropriate components.
Categories of Design Patterns
Design patterns are commonly divided into three major categories:
1. Creational Patterns
These patterns deal with object creation.
Common creational patterns in C# include:
Singleton
Factory Method
Abstract Factory
Builder
Prototype
2. Structural Patterns
These patterns deal with how classes and objects are composed.
Common structural patterns include:
Adapter
Decorator
Facade
Proxy
Composite
Bridge
3. Behavioral Patterns
These patterns deal with communication and responsibility between objects.
Common behavioral patterns include:
Strategy
Observer
Command
Mediator
Chain of Responsibility
State
Template Method
Iterator
1. Singleton Pattern
The Singleton pattern ensures that only one instance of a class is created throughout the application.
It can be useful when exactly one shared instance is required.
Example
public sealed class AppConfiguration
{
private static readonly Lazy<AppConfiguration> _instance =
new(() => new AppConfiguration());
private AppConfiguration()
{
}
public static AppConfiguration Instance => _instance.Value;
public string ApplicationName { get; set; }
}
Usage:
var config = AppConfiguration.Instance;
config.ApplicationName = "My Application";
Important Point
In modern ASP.NET Core applications, Singleton lifetime is usually managed through the built-in Dependency Injection container rather than implementing the Singleton pattern manually.
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
2. Factory Pattern
The Factory pattern provides a way to create objects without exposing the object-creation logic to the calling code.
For example, suppose an application supports multiple notification providers:
Email
SMS
Push Notification
Instead of directly creating implementations everywhere, a factory can determine which implementation should be used.
public interface INotification
{
void Send(string message);
}
public class EmailNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"Email: {message}");
}
}
public class SmsNotification : INotification
{
public void Send(string message)
{
Console.WriteLine($"SMS: {message}");
}
}
Factory:
public class NotificationFactory
{
public INotification Create(string type)
{
return type.ToLower() switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
_ => throw new ArgumentException("Invalid notification type")
};
}
}
The client does not need to know how the objects are created.
3. Abstract Factory Pattern
Abstract Factory is used when we need to create families of related objects without specifying their concrete classes.
For example:
Windows UI
├── Windows Button
└── Windows Checkbox
Mac UI
├── Mac Button
└── Mac Checkbox
The client works with interfaces rather than concrete implementations.
This pattern is useful when an application supports multiple product families or platforms.
4. Builder Pattern
The Builder pattern is useful when an object has many optional properties or complex construction steps.
Instead of using a constructor with many parameters:
var employee = new Employee(
"Bhuwan",
"Developer",
7,
"India",
true,
...
);
we can use a builder:
var employee = new EmployeeBuilder()
.SetName("Bhuwan")
.SetRole("Software Developer")
.SetExperience(7)
.SetLocation("India")
.Build();
Builder improves:
Readability
Maintainability
Object construction
Handling optional properties
5. Repository Pattern
Repository Pattern abstracts database operations from business logic.
Instead of directly writing Entity Framework Core queries inside business services:
var students = await _context.Students
.Where(x => x.IsActive)
.ToListAsync();
we can create:
public interface IStudentRepository
{
Task<List<Student>> GetActiveStudentsAsync();
}
Implementation:
public class StudentRepository : IStudentRepository
{
private readonly ApplicationDbContext _context;
public StudentRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<Student>> GetActiveStudentsAsync()
{
return await _context.Students
.Where(x => x.IsActive)
.ToListAsync();
}
}
This provides better separation between:
Controller
↓
Service / Application Layer
↓
Repository
↓
Entity Framework Core
↓
Database
6. Unit of Work Pattern
Unit of Work coordinates multiple database operations as a single transaction.
For example:
Create Student
↓
Create Student Fee
↓
Create Attendance
↓
Commit Transaction
If one operation fails, the entire transaction can be rolled back.
This is particularly useful when multiple repositories need to participate in the same transaction.
7. Strategy Pattern
Strategy Pattern allows us to define multiple algorithms or business rules and select one at runtime.
For example, different payment methods:
public interface IPaymentStrategy
{
void Pay(decimal amount);
}
Implementations:
public class CreditCardPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine("Paid using Credit Card");
}
}
public class UpiPayment : IPaymentStrategy
{
public void Pay(decimal amount)
{
Console.WriteLine("Paid using UPI");
}
}
The application can select the appropriate strategy without changing the main business logic.
This is useful for:
Payment processing
Tax calculation
Discount calculation
Authentication providers
Notification providers
File processing
8. Observer Pattern
Observer Pattern is used when one object needs to notify multiple dependent objects when its state changes.
Example:
Student Attendance Updated
↓
Attendance Service
↓ ↓ ↓
Parent Teacher Admin
In .NET applications, similar concepts can be implemented using:
Events
Delegates
Messaging systems
Domain events
Azure Service Bus
Message brokers
9. Decorator Pattern
Decorator allows us to add additional functionality to an existing object without modifying its original implementation.
For example:
IStudentService
↓
StudentService
↓
LoggingDecorator
↓
CachingDecorator
This can be useful for adding:
Logging
Caching
Authorization
Validation
Performance monitoring
without changing the core service.
10. Adapter Pattern
Adapter converts the interface of an existing class into another interface expected by the application.
For example, if our application expects:
public interface IPaymentService
{
void ProcessPayment(decimal amount);
}
but a third-party payment provider exposes:
ThirdPartyPayment.MakeTransaction();
an Adapter can bridge the two interfaces.
Application
↓
IPaymentService
↓
PaymentAdapter
↓
Third-Party Payment API
This is particularly useful when integrating:
Legacy systems
Third-party APIs
External payment gateways
Existing libraries
11. Facade Pattern
Facade provides a simple interface over a complex subsystem.
For example, creating an order might involve:
Validate Customer
↓
Check Inventory
↓
Calculate Price
↓
Process Payment
↓
Create Order
↓
Send Notification
Instead of exposing all these operations to the controller, we can create:
_orderFacade.CreateOrder(request);
The Facade hides the complexity from the caller.
12. Mediator Pattern
Mediator reduces direct dependencies between multiple objects by allowing them to communicate through a mediator.
In .NET applications, MediatR is commonly used to implement the mediator concept.
A common architecture is:
Controller
↓
Command
↓
MediatR
↓
Command Handler
↓
Repository / Service
↓
Database
For example:
public record CreateStudentCommand(
string Name,
string Email
) : IRequest<int>;
Handler:
public class CreateStudentCommandHandler
: IRequestHandler<CreateStudentCommand, int>
{
public async Task<int> Handle(
CreateStudentCommand request,
CancellationToken cancellationToken)
{
// Business logic
return 1;
}
}
Mediator is frequently used with CQRS architecture.
13. Command Pattern
Command Pattern encapsulates an operation as an object.
For example:
CreateStudentCommand
UpdateStudentCommand
DeleteStudentCommand
SendNotificationCommand
This makes operations easier to:
Queue
Log
Retry
Validate
Execute asynchronously
Command patterns are commonly used with CQRS and messaging systems.
14. Chain of Responsibility
This pattern passes a request through a chain of handlers until one handler processes it.
Example:
Request
↓
Authentication
↓
Authorization
↓
Validation
↓
Business Rules
↓
Processing
ASP.NET Core middleware is conceptually similar to a chain of responsibility:
Request
↓
Middleware 1
↓
Middleware 2
↓
Middleware 3
↓
Controller
15. Dependency Injection
Dependency Injection is not technically one of the original Gang of Four design patterns, but it is a very important design principle and architectural technique in modern C# applications.
Instead of a class creating its dependencies:
public class StudentService
{
private readonly StudentRepository _repository;
public StudentService()
{
_repository = new StudentRepository();
}
}
we inject the dependency:
public class StudentService
{
private readonly IStudentRepository _repository;
public StudentService(IStudentRepository repository)
{
_repository = repository;
}
}
Register it:
builder.Services.AddScoped<IStudentRepository, StudentRepository>();
Benefits:
Loose coupling
Better unit testing
Easier replacement of implementations
Better maintainability
Follows Dependency Inversion Principle
Design Patterns and SOLID Principles
Design patterns work closely with SOLID principles.
S — Single Responsibility Principle
A class should have one reason to change.
O — Open/Closed Principle
Software should be open for extension but closed for modification.
L — Liskov Substitution Principle
Derived classes should be substitutable for their base classes.
I — Interface Segregation Principle
Clients should not be forced to depend on interfaces they do not use.
D — Dependency Inversion Principle
High-level modules should depend on abstractions rather than concrete implementations.
For example:
Business Logic
↓
Interface
↓
Repository Implementation
instead of:
Business Logic
↓
Concrete Repository
Design Patterns in a Real ASP.NET Core Application
A real enterprise application may combine several patterns:
Angular
↓
ASP.NET Core Web API
↓
Controller
↓
Mediator / CQRS
↓
Command / Query Handler
↓
Service
↓
Repository
↓
Entity Framework Core
↓
SQL Server / PostgreSQL
Additional patterns can be used for:
Factory → Object creation
Strategy → Business algorithms
Decorator → Logging / caching
Adapter → Third-party integrations
Facade → Complex workflows
Observer → Notifications/events
Unit of Work → Transactions
DI → Dependency management
When Should You Use Design Patterns?
Design patterns should not be used simply because they exist.
Use a pattern when it solves a real problem.
For example:
Problem: Multiple payment providers.
Solution: Strategy or Factory.
Problem: Complex object creation.
Solution: Builder.
Problem: Multiple database operations in one transaction.
Solution: Unit of Work.
Problem: Third-party API has an incompatible interface.
Solution: Adapter.
Problem: Complex subsystem needs a simple API.
Solution: Facade.
Problem: Multiple components need to react to an event.
Solution: Observer / Domain Events.
Problem: Need to separate commands and queries.
Solution: CQRS + Mediator.
Key Interview Point
A good developer should understand that design patterns are tools, not rules.
Using too many patterns can make an application unnecessarily complicated.
The goal is not:
"Use as many design patterns as possible."
The goal is:
"Use the simplest appropriate design that keeps the code maintainable, testable, loosely coupled, and extensible."
In modern C#/.NET development, design patterns are often combined with SOLID principles, Dependency Injection, Clean Architecture, CQRS, Repository, Unit of Work, Middleware, Domain Events, and Microservices architecture to build scalable enterprise applications.

Comments
Post a Comment