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

Popular posts from this blog

Refresh an Angular Component without reloading the same Component

.NET Core vs .NET Framework: How to Pick a .NET Runtime for an Application

multi color initial(Avatars) if img not available in Angular it's work angular 2,4+ all versions