When we work on large applications, we often face similar problems again and again. Design patterns help us solve these recurring problems using proven and reusable approaches.
A design pattern is not a piece of code that you copy and paste. Instead, it is a general solution or idea that you can adapt to your own project based on its needs. You follow the concept of the pattern and implement it in a way that fits your application.
Types of Design Patterns
There are mainly three types of design patterns:
- Creational patterns – focus on object creation
- Structural patterns – focus on class and object composition
- Behavioral patterns – focus on communication between objects
In real-world applications, these patterns are not used in isolation. They are often combined to build scalable and maintainable systems.
A good real-world example of this is nopCommerce.
nopCommerce uses many design patterns across different layers of its architecture.
In this article, I will explain common C# design patterns by showing how they are actually used inside nopCommerce.
nopCommerce is divided into three main layers:
- Nop.Core
- Nop.Data
- Nop.Services
I’ll explain two design patterns from each layer.
Nop.Core
Nop.Core contains the core abstractions and infrastructure of nopCommerce.
Design patterns used:
- Singleton Pattern
- Observer Pattern
Singleton Pattern
The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global access point to that instance.
Singleton in nopCommerce
In nopCommerce, there is a custom Singleton class. This implementation is slightly different from the classic GoF Singleton. It works more like a central registry for shared instances during the application’s lifetime.
Core Singleton Implementation
namespace Nop.Core.Infrastructure
{
/// <summary>
/// A statically compiled "singleton" used to store objects
/// throughout the lifetime of the app domain.
/// </summary>
/// <typeparam name="T">The type of object to store.</typeparam>
public partial class Singleton<T> : BaseSingleton
{
private static T instance;
/// <summary>
/// The singleton instance for the specified type T.
/// </summary>
public static T Instance
{
get => instance;
set
{
instance = value;
AllSingletons[typeof(T)] = value;
}
}
}
}
How this works
Singleton stores a single instance of type T
The instance is stored in a static field
All singleton instances are tracked using a shared dictionary
This ensures one shared instance per type for the entire app domain
This approach gives nopCommerce a consistent and controlled way to manage shared objects across the system.
Observer Pattern
The Observer pattern is a behavioral design pattern that allows objects to subscribe to events and get notified automatically when something changes.
Observer in nopCommerce
nopCommerce uses the Observer pattern heavily through its event system. This is especially useful when entities are:
- Inserted
- Updated
- Deleted
Instead of tightly coupling logic everywhere, nopCommerce publishes events and lets consumers react to them.
This keeps the code modular, decoupled, and maintainable.
Core Components of the Observer Pattern in nopCommerce
Events – represent something that happened
Event Publisher – publishes events
Event Consumers – handle the events
Example: Entity Deleted Event
namespace Nop.Core.Events
{
public partial class EntityDeletedEvent<T> where T : BaseEntity
{
public EntityDeletedEvent(T entity)
{
Entity = entity;
}
public T Entity { get; }
}
}
This event carries the deleted entity so consumers know exactly what was deleted.
nopCommerce also provides:
EntityInsertedEvent<T>
EntityUpdatedEvent<T>
Event Publisher Interface
namespace Nop.Core.Events
{
public partial interface IEventPublisher
{
Task PublishAsync<TEvent>(TEvent @event);
void Publish<TEvent>(TEvent @event);
}
}
The publisher sends events to all subscribed consumers, either synchronously or asynchronously.
Nop.Data
Nop.Data is responsible for data access in nopCommerce.
Design patterns used:
- Builder Pattern
- Repository Pattern
- Builder Pattern
The Builder pattern is a creational pattern that helps construct complex objects step by step. It allows different representations of an object using the same construction process.
Builder in nopCommerce
In nopCommerce, the Builder pattern is commonly used with FluentMigrator to define database schemas in a readable and structured way.
Example: BlogPostBuilder
using FluentMigrator.Builders.Create.Table;
using Nop.Core.Domain.Blogs;
using Nop.Core.Domain.Localization;
using Nop.Data.Extensions;
namespace Nop.Data.Mapping.Builders.Blogs
{
/// <summary>
/// Represents a blog post entity builder
/// </summary>
public partial class BlogPostBuilder : NopEntityBuilder<BlogPost>
{
public override void MapEntity(CreateTableExpressionBuilder table)
{
table
.WithColumn(nameof(BlogPost.Title)).AsString(int.MaxValue).NotNullable()
.WithColumn(nameof(BlogPost.Body)).AsString(int.MaxValue).NotNullable()
.WithColumn(nameof(BlogPost.MetaKeywords)).AsString(400).Nullable()
.WithColumn(nameof(BlogPost.MetaTitle)).AsString(400).Nullable()
.WithColumn(nameof(BlogPost.LanguageId)).AsInt32().ForeignKey<Language>();
}
}
}
Why this is Builder Pattern
The object (table schema) is built step by step
Fluent syntax improves readability
The construction logic is separated from usage
This is a good example of Builder + Fluent Interface working together.
Repository Pattern
The Repository pattern abstracts data access logic and provides a clean API for the business layer.
In nopCommerce, repositories:
Hide database details
Improve testability
Promote separation of concerns
Repositories are commonly used with:
- Dependency Injection
- Unit of Work
- Service Layer
Example: Repository Used Inside a Service
public partial class BlogService : IBlogService
{
private readonly IRepository<BlogComment> _blogCommentRepository;
private readonly IRepository<BlogPost> _blogPostRepository;
public BlogService(
IRepository<BlogComment> blogCommentRepository,
IRepository<BlogPost> blogPostRepository)
{
_blogCommentRepository = blogCommentRepository;
_blogPostRepository = blogPostRepository;
}
public virtual async Task DeleteBlogPostAsync(BlogPost blogPost)
{
await _blogPostRepository.DeleteAsync(blogPost);
}
}
Here:
Repository handles data access
Service handles business logic
Nop.Services
Nop.Services contains the business logic layer.
Design patterns used:
- Service Pattern
- Dependency Injection Pattern
- Service Pattern
The Service pattern organizes business logic and acts as a bridge between:
Presentation layer
Data access layer
This makes the application:
Easier to maintain
Easier to test
Easier to understand
Service Interface and Implementation
public partial interface IBlogService
{
Task DeleteBlogPostAsync(BlogPost blogPost);
Task<BlogPost> GetBlogPostByIdAsync(int blogPostId);
}
public partial class BlogService : IBlogService
{
private readonly IRepository<BlogPost> _blogPostRepository;
public BlogService(IRepository<BlogPost> blogPostRepository)
{
_blogPostRepository = blogPostRepository;
}
public async Task DeleteBlogPostAsync(BlogPost blogPost)
{
await _blogPostRepository.DeleteAsync(blogPost);
}
public async Task<BlogPost> GetBlogPostByIdAsync(int blogPostId)
{
return await _blogPostRepository.GetByIdAsync(blogPostId, cache => default);
}
}
The service defines what the system can do, while repositories handle how data is accessed.
Dependency Injection Pattern
Dependency Injection (DI) is used everywhere in nopCommerce.
Instead of creating dependencies manually, they are injected through constructors.
public partial class BlogService : IBlogService
{
private readonly IRepository<BlogPost> _blogPostRepository;
public BlogService(IRepository<BlogPost> blogPostRepository)
{
_blogPostRepository = blogPostRepository;
}
}
Benefits of DI
- Loose coupling
- Easier unit testing
- Better maintainability
- Cleaner architecture
nopCommerce uses DI in:
- Services
- Factories
- Controllers
- Repositories
Disadvantages of Design Patterns
While design patterns are powerful, they are not always the best solution.
Common disadvantages
Complexity – can make code harder to understand
Learning curve – requires solid understanding
Overengineering – patterns used where simple code is enough
Rigidity – may reduce flexibility
Increased development time – more planning and structure
Misapplication – wrong pattern causes bad architecture
Dependency complexity – harder to refactor
Documentation overhead – patterns must be well documented
Design patterns should be used as tools, not rules.
Conclusion
nopCommerce is a great real-world example of how C# design patterns are used in production systems.
Patterns like:
- Singleton
- Observer
- Builder
- Repository
- Service
- Dependency Injection
help keep the codebase scalable, testable, and maintainable.
The key lesson is not to use patterns everywhere, but to use them when they actually solve a problem.
Top comments (0)