If you are learning .NET Core / ASP.NET Core, you will eventually hear a term called Design Pattern.
At first, it may sound complicated:
"What exactly is a design pattern? Why do we need it? Is it some kind of framework?"
Don't worry. It is actually much simpler than it sounds.
In this article, we will learn Design Patterns in .NET Core from the beginning, assuming you are a beginner in programming architecture.
1. What is a Design Pattern?
A Design Pattern is a commonly used solution to a common programming problem.
Think about building a house.
You don't invent everything from zero every time you build a house. There are already common ways to design:
- Doors
- Windows
- Rooms
- Electrical wiring
- Plumbing
You can change the details, but the basic approaches are already known.
Design patterns work similarly in software development.
They give us a proven approach for solving common software design problems.
Simple Definition
A Design Pattern is a reusable solution or approach for solving a commonly occurring software design problem.
It is important to understand that a design pattern is not ready-made code.
It is more like a blueprint or idea that tells us how different classes and objects should work together.
2. Why Do We Need Design Patterns?
Imagine you are creating a small application.
Initially, you may write everything in one class:
public class OrderService
{
public void CreateOrder()
{
// Create order
// Save order
// Send email
// Send SMS
// Process payment
// Generate invoice
}
}
It works.
But what happens when your application becomes large?
You might have:
Order
Payment
Email
SMS
Invoice
Product
User
Notification
Database
Reports
If everything is inside a few large classes, the application becomes difficult to:
- Understand
- Test
- Maintain
- Modify
- Debug
- Extend
This is where design patterns become useful.
3. Benefits of Design Patterns
Design patterns can help us write code that is:
1. Maintainable
It becomes easier to modify existing code.
2. Reusable
The same design approach can be used in different parts of an application.
3. Testable
Dependencies can be replaced with mocks or fake implementations.
4. Scalable
The architecture can handle increasing application complexity.
5. Understandable
Developers who know common patterns can understand the architecture more quickly.
4. Types of Design Patterns
Design patterns are commonly divided into three major categories.
Design Patterns
│
├── Creational Patterns
│
├── Structural Patterns
│
└── Behavioral Patterns
Let's understand each category.
5. Creational Design Patterns
Creational patterns deal with object creation.
Instead of creating objects everywhere in your application, these patterns provide better ways to create and manage objects.
Common examples include:
- Singleton
- Factory
- Abstract Factory
- Builder
- Prototype
6. Singleton Pattern
The Singleton Pattern ensures that a class has only one instance, or one shared instance, during the intended lifetime.
Simple Example
Suppose your application has a configuration manager.
You don't necessarily want every part of the application to create a completely separate configuration object.
Conceptually:
Application
|
v
Configuration
|
+---- Controller
+---- Service
+---- Repository
All can use the same configured instance.
Singleton in .NET Core
ASP.NET Core has built-in Dependency Injection.
We can register a service as Singleton:
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
Now .NET manages the instance for us.
Example:
public interface IConfigurationService
{
string GetValue(string key);
}
Implementation:
public class ConfigurationService : IConfigurationService
{
public string GetValue(string key)
{
return $"Value for {key}";
}
}
Registration:
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
Then we can inject it:
public class HomeController : Controller
{
private readonly IConfigurationService _configurationService;
public HomeController(
IConfigurationService configurationService)
{
_configurationService = configurationService;
}
public IActionResult Index()
{
var value = _configurationService.GetValue("AppName");
return Ok(value);
}
}
Important
In modern .NET applications, you generally should not manually implement Singleton with static variables when the built-in Dependency Injection container can manage the lifetime.
7. Dependency Injection and Design Patterns
One of the most important concepts in ASP.NET Core is Dependency Injection (DI).
Suppose we have:
public class OrderService
{
private PaymentService _paymentService;
public OrderService()
{
_paymentService = new PaymentService();
}
}
There is a problem.
OrderService is directly creating PaymentService.
This creates tight coupling.
Instead:
public class OrderService
{
private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService)
{
_paymentService = paymentService;
}
}
Now OrderService doesn't care how the payment service is created.
ASP.NET Core provides it through DI.
Registration:
builder.Services.AddScoped<IPaymentService, PaymentService>();
This makes our application easier to test and maintain.
8. Factory Pattern
The Factory Pattern is used when we want to create objects without exposing the exact object creation logic to the calling code.
Imagine we support multiple payment methods:
Payment
│
├── Credit Card
├── UPI
├── PayPal
└── Wallet
Without a Factory:
if (paymentType == "UPI")
{
// Create UPI payment
}
else if (paymentType == "Card")
{
// Create card payment
}
else if (paymentType == "PayPal")
{
// Create PayPal payment
}
As payment methods increase, this code becomes messy.
Factory Example
Create an interface:
public interface IPayment
{
void Pay(decimal amount);
}
UPI:
public class UpiPayment : IPayment
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ₹{amount} using UPI");
}
}
Card:
public class CardPayment : IPayment
{
public void Pay(decimal amount)
{
Console.WriteLine($"Paid ₹{amount} using Card");
}
}
Factory:
public class PaymentFactory
{
public IPayment CreatePayment(string type)
{
return type.ToLower() switch
{
"upi" => new UpiPayment(),
"card" => new CardPayment(),
_ => throw new ArgumentException("Invalid payment type")
};
}
}
Usage:
var factory = new PaymentFactory();
IPayment payment = factory.CreatePayment("upi");
payment.Pay(1000);
Output:
Paid ₹1000 using UPI
The calling code doesn't need to know how UpiPayment is created.
9. Builder Pattern
The Builder Pattern is useful when creating a complex object with many optional properties.
For example:
var user = new User
{
Name = "Rahul",
Email = "rahul@example.com",
Phone = "9999999999",
Address = "Delhi",
Age = 25
};
This may become difficult when an object has many configuration options.
A Builder can make the creation process clearer.
Example:
public class UserBuilder
{
private readonly User _user = new();
public UserBuilder SetName(string name)
{
_user.Name = name;
return this;
}
public UserBuilder SetEmail(string email)
{
_user.Email = email;
return this;
}
public UserBuilder SetAge(int age)
{
_user.Age = age;
return this;
}
public User Build()
{
return _user;
}
}
Usage:
var user = new UserBuilder()
.SetName("Rahul")
.SetEmail("rahul@example.com")
.SetAge(25)
.Build();
This is especially useful when object construction has multiple steps or optional configuration.
10. Structural Design Patterns
Structural patterns deal with how classes and objects are connected.
Common examples include:
- Adapter
- Decorator
- Facade
- Proxy
- Composite
Let's understand the most useful ones.
11. Adapter Pattern
Suppose your application expects:
public interface IPayment
{
void Pay(decimal amount);
}
But you have an external payment SDK that provides:
public class ExternalPayment
{
public void MakePayment(decimal amount)
{
Console.WriteLine($"Payment completed: {amount}");
}
}
Our application expects:
Pay()
but the external library provides:
MakePayment()
We can use an Adapter.
public class PaymentAdapter : IPayment
{
private readonly ExternalPayment _externalPayment;
public PaymentAdapter(ExternalPayment externalPayment)
{
_externalPayment = externalPayment;
}
public void Pay(decimal amount)
{
_externalPayment.MakePayment(amount);
}
}
Now our application can use:
IPayment payment =
new PaymentAdapter(new ExternalPayment());
payment.Pay(500);
The Adapter converts one interface into another interface that our application expects.
12. Decorator Pattern
The Decorator Pattern allows us to add additional behavior to an existing object without changing its original class.
Suppose we have:
public interface IOrderService
{
void CreateOrder();
}
Basic implementation:
public class OrderService : IOrderService
{
public void CreateOrder()
{
Console.WriteLine("Order created");
}
}
Now we want logging.
Instead of modifying OrderService, we can create a decorator:
public class LoggingOrderService : IOrderService
{
private readonly IOrderService _orderService;
public LoggingOrderService(IOrderService orderService)
{
_orderService = orderService;
}
public void CreateOrder()
{
Console.WriteLine("Creating order...");
_orderService.CreateOrder();
Console.WriteLine("Order creation completed");
}
}
Now:
LoggingOrderService
|
v
OrderService
The original service remains unchanged.
13. Facade Pattern
A Facade provides a simple interface to a complicated system.
Imagine placing an order requires:
Order Service
|
+-- Payment Service
|
+-- Inventory Service
|
+-- Notification Service
|
+-- Invoice Service
Instead of the controller calling all of these services individually, we can create:
public class OrderFacade
{
public void PlaceOrder()
{
// Payment
// Inventory
// Invoice
// Notification
}
}
The controller simply does:
_orderFacade.PlaceOrder();
The complexity is hidden behind the Facade.
14. Behavioral Design Patterns
Behavioral patterns focus on how objects communicate and how responsibilities are distributed.
Common examples include:
- Strategy
- Observer
- Command
- Chain of Responsibility
- Mediator
- State
15. Strategy Pattern
The Strategy Pattern is very useful in real-world applications.
Suppose an e-commerce application has different discount strategies:
Discount
│
├── Festival Discount
├── VIP Discount
├── Coupon Discount
└── No Discount
Instead of writing:
if (type == "festival")
{
}
else if (type == "vip")
{
}
else if (type == "coupon")
{
}
we can create different strategies.
Interface:
public interface IDiscountStrategy
{
decimal CalculateDiscount(decimal amount);
}
Festival:
public class FestivalDiscount : IDiscountStrategy
{
public decimal CalculateDiscount(decimal amount)
{
return amount * 0.20m;
}
}
VIP:
public class VipDiscount : IDiscountStrategy
{
public decimal CalculateDiscount(decimal amount)
{
return amount * 0.30m;
}
}
Now our application can select the required strategy.
public class DiscountService
{
private readonly IDiscountStrategy _strategy;
public DiscountService(IDiscountStrategy strategy)
{
_strategy = strategy;
}
public decimal GetDiscount(decimal amount)
{
return _strategy.CalculateDiscount(amount);
}
}
The important idea is:
We can change the algorithm without changing the main business logic.
16. Observer Pattern
The Observer Pattern is useful when one event needs to notify multiple objects.
Imagine an order is created.
We may need to:
Order Created
|
+---- Send Email
|
+---- Send SMS
|
+---- Send Push Notification
|
+---- Update Analytics
Instead of tightly connecting everything to the Order class, we can use an event/observer approach.
In .NET, this concept appears in many forms, including:
- Events
- Event handlers
- Messaging systems
- Domain events
- Pub/Sub systems
For example:
public class OrderService
{
public event EventHandler? OrderCreated;
public void CreateOrder()
{
Console.WriteLine("Order Created");
OrderCreated?.Invoke(this, EventArgs.Empty);
}
}
Another component can subscribe:
orderService.OrderCreated += SendEmail;
This allows one event to notify multiple listeners.
17. Repository Pattern
If you work with ASP.NET Core and Entity Framework Core, you will often hear about the Repository Pattern.
The idea is to separate database access from business logic.
Without separation:
public class ProductService
{
public Product GetProduct(int id)
{
// Direct database code
}
}
With a repository:
Controller
|
v
Service
|
v
Repository
|
v
Database
Interface:
public interface IProductRepository
{
Product? GetById(int id);
IEnumerable<Product> GetAll();
}
Implementation:
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public Product? GetById(int id)
{
return _context.Products
.FirstOrDefault(x => x.Id == id);
}
public IEnumerable<Product> GetAll()
{
return _context.Products.ToList();
}
}
Then the service uses the abstraction:
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
public Product? GetProduct(int id)
{
return _repository.GetById(id);
}
}
18. Important Note About Repository Pattern in EF Core
There is an important architectural discussion here.
Entity Framework Core already provides abstractions such as:
DbContext
DbSet<T>
So adding a generic repository on top of EF Core is not automatically a best practice.
For simple applications, directly using DbContext inside application services can be perfectly reasonable.
Repositories become more useful when they provide meaningful domain-specific data-access behavior or when the architecture has a clear reason for the abstraction.
Don't create a pattern just because someone says:
"Every .NET project must have Repository Pattern."
Patterns should solve problems, not create extra code.
19. Unit of Work Pattern
The Unit of Work Pattern groups multiple database operations into one logical transaction.
Imagine creating an order:
Create Order
|
+-- Save Order
|
+-- Update Inventory
|
+-- Create Payment Record
We generally want these changes to succeed together.
Conceptually:
BEGIN TRANSACTION
Create Order
Update Inventory
Create Payment
COMMIT
If something fails:
ROLLBACK
EF Core's DbContext already provides behavior that can cover Unit-of-Work responsibilities in many applications.
Therefore, just like Repository Pattern, don't add a separate Unit of Work abstraction automatically.
20. Dependency Injection Lifetimes in ASP.NET Core
ASP.NET Core provides three common DI lifetimes.
Singleton
One instance for the application's service container lifetime.
builder.Services.AddSingleton<IMyService, MyService>();
Use it for services that are:
- Stateless
- Thread-safe
- Safe to share
Scoped
One instance per scope.
In a typical ASP.NET Core web request, that usually means one instance per HTTP request.
builder.Services.AddScoped<IMyService, MyService>();
This is commonly used for services working with:
DbContext
Business Services
Repositories
Transient
A new instance is created each time it is requested.
builder.Services.AddTransient<IMyService, MyService>();
Useful for lightweight, stateless services where sharing an instance is unnecessary.
21. Design Patterns and SOLID Principles
Design patterns work very closely with SOLID principles.
SOLID stands for:
S → Single Responsibility Principle
O → Open/Closed Principle
L → Liskov Substitution Principle
I → Interface Segregation Principle
D → Dependency Inversion Principle
For example, consider:
public class OrderService
{
public void CreateOrder()
{
// Order logic
// Payment logic
// Email logic
// Invoice logic
}
}
This class has too many responsibilities.
The Single Responsibility Principle suggests separating these concerns.
We might have:
OrderService
PaymentService
EmailService
InvoiceService
Then Dependency Injection connects them.
This produces a cleaner architecture.
22. How Patterns Fit Together in a Real ASP.NET Core Application
A production application might look something like this:
Client
|
v
Controller
|
v
Application
Service
|
+--------+--------+
| | |
v v v
Repository Payment Notification
| | |
v v v
Database Gateway Email/SMS
Different patterns may be used at different levels.
For example:
Dependency Injection
↓
Service Layer
↓
Repository
↓
Database
And inside the service:
Strategy
Factory
Facade
Decorator
The exact architecture depends on the application.
23. Example: E-Commerce Application
Let's take a practical example.
Suppose we are building an e-commerce application.
A customer places an order.
The flow could be:
Customer
|
v
OrderController
|
v
OrderService
|
+---- Discount Strategy
|
+---- Payment Factory
|
+---- Inventory Repository
|
+---- Notification Service
|
v
Database
Step 1 — Controller
Receives the HTTP request.
[HttpPost]
public IActionResult CreateOrder(CreateOrderRequest request)
{
_orderService.CreateOrder(request);
return Ok();
}
Step 2 — Service
Handles business logic.
public void CreateOrder(CreateOrderRequest request)
{
// Validate order
// Calculate discount
// Process payment
// Update inventory
// Save order
// Notify customer
}
Step 3 — Repository
Handles database-related operations.
_repository.Save(order);
Step 4 — Strategy
Calculates the appropriate discount.
var discount = strategy.CalculateDiscount(amount);
Step 5 — Factory
Creates the appropriate payment implementation.
var payment = paymentFactory.CreatePayment("upi");
Now every component has a clear responsibility.
24. Which Design Patterns Should a Beginner Learn First?
Don't try to memorize 20+ patterns at once.
Start with these:
Level 1 — Must Know
1. Dependency Injection
2. Factory
3. Strategy
4. Repository
5. Singleton
Level 2 — Learn Next
6. Decorator
7. Adapter
8. Facade
9. Builder
10. Unit of Work
Level 3 — Advanced
11. Mediator
12. Command
13. Observer
14. Chain of Responsibility
15. State
The exact order can vary depending on the type of project you are building.
25. Don't Overuse Design Patterns
This is one of the most important lessons.
A beginner may learn design patterns and then think:
"I need to use a pattern everywhere."
That's a mistake.
For example, if your application has:
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
You probably don't need:
CalculatorFactory
CalculatorRepository
CalculatorStrategy
CalculatorFacade
CalculatorBuilder
CalculatorManager
CalculatorProvider
That would make simple code unnecessarily complicated.
A good developer asks:
"What problem am I solving?"
Then chooses a pattern if it actually helps.
26. Design Pattern vs Framework Feature
Another important point:
A framework feature and a design pattern are not always the same thing.
For example, ASP.NET Core provides:
Dependency Injection
Middleware
Configuration
Logging
Routing
Authentication
Authorization
Some of these features are implemented using established software design concepts and patterns internally.
As a developer, you don't always need to manually implement the underlying pattern.
For example, instead of manually implementing Singleton, ASP.NET Core DI can manage service lifetimes:
builder.Services.AddSingleton<IMyService, MyService>();
Use the framework's capabilities when they already solve the problem well.
27. A Simple Rule to Remember
You can remember design patterns like this:
CREATIONAL
"How should I create objects?"
↓
Factory
Builder
Singleton
STRUCTURAL
"How should objects/classes be connected?"
↓
Adapter
Decorator
Facade
BEHAVIORAL
"How should objects communicate or behave?"
↓
Strategy
Observer
Command
Mediator
This simple classification makes it much easier to understand them.
28. Final Example
Let's imagine we are building a food delivery application.
We might have:
API
|
v
Controller
|
v
OrderService
|
+---------+---------+
| | |
v v v
Strategy Factory Repository
| | |
v v v
Discount Payment Database
Gateway
Here:
Dependency Injection
connects our components.
Strategy Pattern
handles different discount algorithms.
Factory Pattern
creates different payment implementations.
Repository Pattern
separates data access.
Decorator Pattern
can add logging, caching, etc.
Facade Pattern
can simplify complicated workflows.
This is how patterns can work together in a real application.
Conclusion
Design Patterns are not magic code and they are not rules that every .NET Core application must follow.
They are proven approaches for solving recurring software design problems.
If you are a beginner in .NET Core, don't try to memorize every pattern.
First understand:
Problem
↓
Why is the current code difficult?
↓
What responsibility should be separated?
↓
Which pattern can help?
↓
Implement only what is necessary
Start with Dependency Injection, Factory, Strategy, Repository, and Singleton, then gradually learn more advanced patterns such as Decorator, Adapter, Facade, Mediator, and Command.
The real goal is not to say:
"I know 20 design patterns."
The real goal is to be able to say:
"I understand the problem, and I know which design approach will make my code cleaner, maintainable, testable, and easier to extend."
That is what good software design is about.
Top comments (0)