DEV Community

Aezan
Aezan

Posted on

Design Patterns Explained in C# & nopCommerce

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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

Thanks for reading!
You can find me on GitHub
and LinkedIn

Top comments (0)