A concise, easy-to-follow guide to the core Software Design Principles, explained with simple, real-world C# examples and short, plain-language descriptions.
Design principles are a set of guidelines that help make software designs more understandable, flexible, and maintainable. They do not prescribe any specific implementation, nor are they bound to a particular programming language, so they can be applied regardless of the language or technology stack you use.
While there are many design principles, SOLID (SRP, OCP, LSP, ISP, DIP) is the most fundamental set. Alongside SOLID, several other principles help us avoid poor designs:
- DRY – Don't Repeat Yourself
- Once and Only Once
- The Law of Demeter
- Package Principles
- YAGNI – You Aren't Gonna Need It
SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible, and maintainable.
| Letter | Principle | One-liner | Typical violation |
|---|---|---|---|
| S | Single Responsibility Principle (SRP) | One class = one reason to change | A class that validates emails, saves data, and sends emails |
| O | Open-Closed Principle (OCP) | Open for extension, closed for modification | Adding a new type requires editing if/else chains |
| L | Liskov Substitution Principle (LSP) | Subtypes must be substitutable for their base type | A subclass throws for a method its base type promises |
| I | Interface Segregation Principle (ISP) | Clients should not be forced to use methods they don't need | A "fat" interface with methods most classes throw for |
| D | Dependency Inversion Principle (DIP) | Depend on abstractions, not concretions | A high-level class creates its own low-level dependency |
The Single Responsibility Principle states that every software module (class, function) should have only one reason to change.
This means that every module (class or method) in your code should have only one job. Everything in that module should relate to a single purpose. It should not be like a Swiss Army knife, where changing one tool requires altering the entire tool.
This does not mean that a class should contain only one method or property. A class may have many members, as long as they all relate to a single responsibility. Think of it like buying a separate knife, nail cutter, and screwdriver: each tool is simple and easy to maintain, and changing one does not affect the others.
SRP example (Employee)
Before (violates SRP):
After (follows SRP):
Before (violates SRP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SingleResponsibility
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new Employee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new Employee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
foreach (Employee emp in employeeService.employeeList)
{
Console.WriteLine(
"Employee details: " + emp.CalculateBonus(500)
);
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
public decimal CalculateBonus(decimal salary)
{
decimal bonus;
if (this.employee_type == "Permanent")
{
bonus = (salary * 50) / 100;
}
else
{
bonus = (salary * 25) / 100;
}
return bonus;
}
private bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
SendEmail();
}
catch (Exception ex)
{
throw;
}
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
}
}[!WARNING] This code works, but it does not follow SRP. The
EmployeeandEmployeeServiceclasses are doing things that are not their responsibility: theEmployeeclass should not performValidateEmail, and theEmployeeServiceclass should notSendEmail.
We can implement SRP by removing the responsibilities that are not relevant to Employee and EmployeeService:
After (follows SRP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SingleResponsibility
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new Employee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new Employee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
foreach (Employee emp in employeeService.employeeList)
{
Console.WriteLine(
"Employee details: " + emp.CalculateBonus(500)
);
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
private EmailService emailService = new EmailService();
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (emailService.ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
public decimal CalculateBonus(decimal salary)
{
decimal bonus;
if (this.employee_type == "Permanent")
{
bonus = (salary * 50) / 100;
}
else
{
bonus = (salary * 25) / 100;
}
return bonus;
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
private EmailService emailService = new EmailService();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
emailService.SendEmail();
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
public class EmailService
{
public bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
}
}[!IMPORTANT] Now each class has a single, clear responsibility:
Employeemodels an employee,EmailServicehandles email validation and sending, andEmployeeServicemanages employees.
The Open-Closed Principle states that software entities such as modules, classes, and functions should be open for extension but closed for modification.
This simply means that classes and functions should not be modified whenever we need to develop new features. We should extend the entities, not modify them.
OCP example (Employee)
Before (violates OCP):
After (follows OCP):
Before (violates OCP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace OpenClosed
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new Employee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new Employee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
foreach (Employee emp in employeeService.employeeList)
{
Console.WriteLine(
"Employee details: " + emp.CalculateBonus(500)
);
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
private EmailService emailService = new EmailService();
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (emailService.ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
public decimal CalculateBonus(decimal salary)
{
decimal bonus;
if (this.employee_type == "Permanent")
{
bonus = (salary * 50) / 100;
}
else
{
bonus = (salary * 25) / 100;
}
return bonus;
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
private EmailService emailService = new EmailService();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
emailService.SendEmail();
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
public class EmailService
{
public bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
}
}[!WARNING] This code works, but it does not follow OCP. The problem with the
Employeeclass above is that if we want to add a newemployee_type, we need to add one moreifcondition inside the sameCalculateBonusmethod — in other words, we need to modify theEmployeeclass. If we change theEmployeeclass again and again, we must test both the previous and the new functionalities every time to make sure everything still works.
Implementation guidelines:
- The simplest way to apply OCP is to implement the new functionality in new derived (sub) classes that inherit the original class implementation.
- Another way is to allow the client to access the original class through an abstract interface.
The code below demonstrates how to achieve OCP using abstraction.
After (follows OCP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace OpenClosed
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new PermanentEmployee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new TemporaryEmployee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
foreach (Employee emp in employeeService.employeeList)
{
Console.WriteLine(
"Employee details: " + emp.CalculateBonus(500)
);
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public abstract class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
public abstract decimal CalculateBonus(decimal salary);
private EmailService emailService = new EmailService();
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (emailService.ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public virtual string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
}
public class PermanentEmployee : Employee
{
public PermanentEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public override decimal CalculateBonus(decimal salary)
{
return salary * 50 / 100;
}
}
public class TemporaryEmployee : Employee
{
public TemporaryEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public override decimal CalculateBonus(decimal salary)
{
return salary * 25 / 100;
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
private EmailService emailService = new EmailService();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
emailService.SendEmail();
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
public class EmailService
{
public bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
}
}[!IMPORTANT] Now the
Employeeclass is open for extension: adding a new employee type only requires creating a new derived class and overridingCalculateBonus, without touching the existing code.
The Liskov Substitution Principle states that any derived class should be able to be used in place of its base class without changing the behavior of the program.
This principle is an extension of the Open-Closed Principle. It means that we must ensure that new derived classes extend the base classes without changing their behavior.
LSP example (Employee)
Before (violates LSP):
After (follows LSP):
Before (violates LSP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace LiskovSubstitution
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new PermanentEmployee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new TemporaryEmployee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
Employee cntEmployee = new ContractEmployee(
100003,
"Contract Employee",
"cnt@mail.com",
"Contract",
"HRM",
"1 Year"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
employeeService.AddEmployee(cntEmployee);
foreach (Employee emp in employeeService.employeeList)
{
// ContractEmployee cannot be substituted here:
// its CalculateBonus throws NotImplementedException
Console.WriteLine(
"Employee details: " + emp.CalculateBonus(500)
);
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public abstract class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
public abstract decimal CalculateBonus(decimal salary);
private EmailService emailService = new EmailService();
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (emailService.ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public virtual string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
}
public class PermanentEmployee : Employee
{
public PermanentEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public override decimal CalculateBonus(decimal salary)
{
return salary * 50 / 100;
}
}
public class TemporaryEmployee : Employee
{
public TemporaryEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public override decimal CalculateBonus(decimal salary)
{
return salary * 25 / 100;
}
}
public class ContractEmployee : Employee
{
public string project_name { get; set; }
public string project_duration { get; set; }
public ContractEmployee(
int id,
string name,
string email,
string employee_type,
string project_name,
string project_duration
)
: base(id, name, email, employee_type)
{
this.project_name = project_name;
this.project_duration = project_duration;
}
public override string GetEmployeeDetails()
{
return string.Format(
"Id: {0} Name: {1} project_name: {2}",
id,
name,
this.project_name
);
}
public override decimal CalculateBonus(decimal salary)
{
throw new NotImplementedException();
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
private EmailService emailService = new EmailService();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
emailService.SendEmail();
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
public class EmailService
{
public bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
}
}[!WARNING] If you run this code, it throws
NotImplementedExceptionas soon as the loop reaches theContractEmployee. TheContractEmployeeclass cannot be used in place of its baseEmployeeclass, because bonus is not applicable to aContractEmployee— itsCalculateBonusthrowsNotImplementedException. This is an LSP violation.
Below, we have redefined the code to follow LSP:
After (follows LSP):
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace LiskovSubstitution
{
class Program
{
public static void Main()
{
try
{
Employee perEmployee = new PermanentEmployee(
100001,
"Permanent Employee",
"cnt@mail.com",
"Permanent"
);
Employee tempEmployee = new TemporaryEmployee(
100002,
"Temporary Employee",
"tmp@mail.com",
"Temporary"
);
Employee cntEmployee = new ContractEmployee(
100003,
"Contract Employee",
"cnt@mail.com",
"Contract",
"HRM",
"1 Year"
);
EmployeeService employeeService = new EmployeeService();
employeeService.AddEmployee(perEmployee);
employeeService.AddEmployee(tempEmployee);
employeeService.AddEmployee(cntEmployee);
foreach (Employee emp in employeeService.employeeList)
{
Console.WriteLine(
"Employee details: " + emp.GetEmployeeDetails()
);
if (emp is IEmployeeBonus bonusEligible)
{
Console.WriteLine(
"Employee bonus: " + bonusEligible.CalculateBonus(500)
);
}
}
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class Employee
{
public int id { get; set; }
public string name { get; set; }
public string email { get; set; }
public string employee_type { get; set; }
private EmailService emailService = new EmailService();
public Employee(
int id,
string name,
string email,
string employee_type
)
{
this.id = id;
this.name = name;
this.employee_type = employee_type;
if (emailService.ValidateEmail(email))
{
this.email = email;
}
else
{
throw new EmailException("Invalid email!");
}
}
public virtual string GetEmployeeDetails()
{
return string.Format("Id: {0} Name: {1}", this.id, this.name);
}
}
public interface IEmployeeBonus
{
decimal CalculateBonus(decimal salary);
}
public class PermanentEmployee : Employee, IEmployeeBonus
{
public PermanentEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public decimal CalculateBonus(decimal salary)
{
return salary * 50 / 100;
}
}
public class TemporaryEmployee : Employee, IEmployeeBonus
{
public TemporaryEmployee(
int id,
string name,
string email,
string employee_type
)
: base(id, name, email, employee_type) { }
public decimal CalculateBonus(decimal salary)
{
return salary * 25 / 100;
}
}
public class ContractEmployee : Employee
{
public string project_name { get; set; }
public string project_duration { get; set; }
public ContractEmployee(
int id,
string name,
string email,
string employee_type,
string project_name,
string project_duration
)
: base(id, name, email, employee_type)
{
this.project_name = project_name;
this.project_duration = project_duration;
}
public override string GetEmployeeDetails()
{
return string.Format(
"Id: {0} Name: {1} project_name: {2}",
id,
name,
this.project_name
);
}
}
public class EmployeeService
{
public List<Employee> employeeList = new List<Employee>();
private EmailService emailService = new EmailService();
public void AddEmployee(Employee employee)
{
try
{
employeeList.Add(employee);
emailService.SendEmail();
}
catch (Exception ex)
{
throw;
}
}
}
public class EmailException : Exception
{
public EmailException(string message) : base(message) { }
}
public class EmailService
{
public bool ValidateEmail(string email)
{
const string pattern = @"^[\w.+-]+@[\w-]+(\.[\w-]+)+$";
return Regex.IsMatch(email, pattern);
}
public void SendEmail()
{
try
{
// Code for email setting and sending
}
catch (Exception ex)
{
throw;
}
}
}
}
}[!IMPORTANT] Now
Employeeno longer declaresCalculateBonus. Only the classes that genuinely support bonuses implement theIEmployeeBonusinterface, so everyEmployeesubclass can be safely used wherever anEmployeeis expected — LSP is satisfied.
The Interface Segregation Principle states that clients should not be forced to implement any methods they don't use.
This means that instead of one fat interface, many small interfaces are preferred, each grouping methods that serve a single sub-module.
ISP example (Employee)
Before (violates ISP):
After (follows ISP):
Before (violates ISP):
using System;
namespace InterfaceSegregation
{
class Program
{
public static void Main()
{
try
{
TeamLead teamLead = new TeamLead();
Manager manager = new Manager();
Programmer programmer = new Programmer();
teamLead.WorkOnTask();
// throws - forced to implement a method it does not use
manager.WorkOnTask();
// throws - forced to implement a method it does not use
programmer.AssignTask();
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public interface ILead
{
void CreateSubTask();
void AssignTask();
void WorkOnTask();
}
public class TeamLead : ILead
{
public void AssignTask()
{
Console.WriteLine("TeamLead can assign task");
}
public void CreateSubTask()
{
Console.WriteLine("TeamLead can create sub task");
}
public void WorkOnTask()
{
Console.WriteLine("TeamLead can work on task");
}
}
public class Manager : ILead
{
public void AssignTask()
{
Console.WriteLine("Manager can assign task");
}
public void CreateSubTask()
{
Console.WriteLine("Manager can create sub task");
}
public void WorkOnTask()
{
throw new Exception("Manager does not work on tasks");
}
}
public class Programmer : ILead
{
public void AssignTask()
{
throw new Exception("Programmer does not assign tasks");
}
public void CreateSubTask()
{
throw new Exception("Programmer does not create sub tasks");
}
public void WorkOnTask()
{
Console.WriteLine("Programmer only works on tasks");
}
}
}
}[!WARNING] Here we are forcing the
Managerclass to implementWorkOnTask(), and forcing theProgrammerclass to implementAssignTask()andCreateSubTask(). This is wrong — the design violates ISP.
Let's correct the design:
After (follows ISP):
using System;
namespace InterfaceSegregation
{
class Program
{
public static void Main()
{
try
{
TeamLead teamLead = new TeamLead();
Manager manager = new Manager();
Programmer programmer = new Programmer();
teamLead.WorkOnTask();
manager.AssignTask();
programmer.WorkOnTask();
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public interface IProgrammer
{
void WorkOnTask();
}
public interface ILead
{
void CreateSubTask();
void AssignTask();
}
public class TeamLead : ILead, IProgrammer
{
public void AssignTask()
{
Console.WriteLine("TeamLead can assign task");
}
public void CreateSubTask()
{
Console.WriteLine("TeamLead can create sub task");
}
public void WorkOnTask()
{
Console.WriteLine("TeamLead can work on task");
}
}
public class Manager : ILead
{
public void AssignTask()
{
Console.WriteLine("Manager can assign task");
}
public void CreateSubTask()
{
Console.WriteLine("Manager can create sub task");
}
}
public class Programmer : IProgrammer
{
public void WorkOnTask()
{
Console.WriteLine("Programmer only works on tasks");
}
}
}
}[!IMPORTANT] Now each client depends only on the methods it actually uses:
ILeadfor assigning/creating tasks andIProgrammerfor working on tasks. No class is forced to implement methods it doesn't need.
The Dependency Inversion Principle states that high-level modules/classes should not depend on low-level modules/classes. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
DIP example (Employee)
Scenario at a glance:
| Before (violates DIP) | After (follows DIP) |
|---|---|
EmployeeBusinessLogic (high-level) directly creates its own EmployeeDataAccess (low-level) |
IRepositoryLayer (abstraction) — declares Save / GetEmployeeDetails |
EmployeeBusinessLogic depends on IRepositoryLayer, injected through the constructor |
|
EmployeeDataAccess implements IRepositoryLayer |
Before (violates DIP):
using System;
using System.Collections.Generic;
namespace DependencyInversion
{
class Program
{
public static void Main()
{
try
{
EmployeeViewModel empModel = new EmployeeViewModel();
empModel.ID = 1001;
empModel.Name = "Saiful";
empModel.Salary = 9999;
empModel.Department = "IT";
EmployeeBusinessLogic empBLL = new EmployeeBusinessLogic();
empBLL.Save(empModel);
var result = empBLL.GetEmployeeDetails(1001);
Console.WriteLine(result.Name);
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class EmployeeViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public int Salary { get; set; }
}
public class EmployeeBusinessLogic
{
private readonly EmployeeDataAccess empDAL;
public EmployeeBusinessLogic()
{
empDAL = new EmployeeDataAccess();
}
public void Save(EmployeeViewModel employeeViewModel)
{
Employee emp = new Employee();
emp.ID = employeeViewModel.ID;
emp.Name = employeeViewModel.Name;
emp.Department = employeeViewModel.Department;
emp.Salary = employeeViewModel.Salary;
empDAL.Save(emp);
}
public EmployeeViewModel GetEmployeeDetails(int id)
{
Employee emp = empDAL.GetEmployeeDetails(id);
EmployeeViewModel employeeViewModel = new EmployeeViewModel();
employeeViewModel.ID = emp.ID;
employeeViewModel.Name = emp.Name;
employeeViewModel.Department = emp.Department;
employeeViewModel.Salary = emp.Salary;
return employeeViewModel;
}
}
public class EmployeeDataAccess
{
List<Employee> employeeList = new List<Employee>();
public void Save(Employee employee)
{
// In real time the data will be saved in db
employeeList.Add(employee);
}
public Employee GetEmployeeDetails(int id)
{
// In real time get the employee details from db
return employeeList.Find(employee => employee.ID == id);
}
}
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public int Salary { get; set; }
}
}
}As per the DIP definition, a high-level module should not depend on a low-level module; both should depend on abstraction. First, let's decide which module is high-level and which is low-level. A high-level module is a module that depends on other modules.
In our example, EmployeeBusinessLogic depends on the EmployeeDataAccess class, so EmployeeBusinessLogic is a high-level module and EmployeeDataAccess is a low-level module. As per the first rule of DIP, EmployeeBusinessLogic should not depend on the concrete EmployeeDataAccess class; instead, both classes should depend on an abstraction.
The second rule of DIP states that "abstractions should not depend on details; details should depend on abstractions."
Before understanding this rule, let's clarify what an abstraction is. In simple words, an abstraction is something that is non-concrete. In programming, that means creating either an interface or an abstract class — something we cannot create an instance of. In our example, EmployeeBusinessLogic and EmployeeDataAccess are concrete classes, which means we can create objects from them.
To follow DIP, the EmployeeBusinessLogic (high-level module) should not depend on the concrete EmployeeDataAccess (low-level module) class. Both classes should depend on an abstraction — meaning both should depend on an interface or an abstract class.
To achieve this, we introduce an interface that acts as the abstraction, decoupling EmployeeBusinessLogic and EmployeeDataAccess, and we wire them together using dependency injection.
This decoupling is demonstrated in the diagram below:
The code below demonstrates how to achieve DIP.
After (follows DIP):
using System;
using System.Collections.Generic;
namespace DependencyInversion
{
class Program
{
public static void Main()
{
try
{
EmployeeViewModel empModel = new EmployeeViewModel();
empModel.ID = 1001;
empModel.Name = "Saiful";
empModel.Salary = 9999;
empModel.Department = "IT";
EmployeeBusinessLogic empBLL = new EmployeeBusinessLogic(
new EmployeeDataAccess()
);
empBLL.Save(empModel);
var result = empBLL.GetEmployeeDetails(1001);
Console.WriteLine(result.Name);
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
Console.ReadKey();
}
}
public class EmployeeViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public int Salary { get; set; }
}
public class EmployeeBusinessLogic
{
private readonly IRepositoryLayer empRepository;
public EmployeeBusinessLogic(IRepositoryLayer repositoryLayer)
{
empRepository = repositoryLayer;
}
public void Save(EmployeeViewModel employeeViewModel)
{
Employee emp = new Employee();
emp.ID = employeeViewModel.ID;
emp.Name = employeeViewModel.Name;
emp.Department = employeeViewModel.Department;
emp.Salary = employeeViewModel.Salary;
empRepository.Save(emp);
}
public EmployeeViewModel GetEmployeeDetails(int id)
{
Employee emp = empRepository.GetEmployeeDetails(id);
EmployeeViewModel employeeViewModel = new EmployeeViewModel();
employeeViewModel.ID = emp.ID;
employeeViewModel.Name = emp.Name;
employeeViewModel.Department = emp.Department;
employeeViewModel.Salary = emp.Salary;
return employeeViewModel;
}
}
public class EmployeeDataAccess : IRepositoryLayer
{
List<Employee> employeeList = new List<Employee>();
public void Save(Employee employee)
{
// In real time the data will be saved in db
employeeList.Add(employee);
}
public Employee GetEmployeeDetails(int id)
{
// In real time get the employee details from db
return employeeList.Find(employee => employee.ID == id);
}
}
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public string Department { get; set; }
public int Salary { get; set; }
}
public interface IRepositoryLayer
{
void Save(Employee employee);
Employee GetEmployeeDetails(int id);
}
}
}[!IMPORTANT] Now
EmployeeBusinessLogicdepends on theIRepositoryLayerabstraction instead of the concreteEmployeeDataAccess. Swapping storage implementations (e.g., database, file, in-memory) is as simple as passing a different implementation ofIRepositoryLayer— no changes to the business logic are required.
Dependency Injection (DI) is a technique in which an object receives its dependencies from an external source, rather than creating them itself.
DI is the most common way to implement the Dependency Inversion Principle. Instead of a class instantiating its dependencies with new, the dependencies are injected through the constructor, a property, or a method parameter. This makes the class decoupled from its dependencies and easy to test.
Dependency Injection example (Notification)
Scenario at a glance:
| Component | Role |
|---|---|
IMessenger |
Abstraction — declares SendMessage() |
Email : IMessenger |
Sends an email |
SMS : IMessenger |
Sends an SMS |
Notification |
Receives an IMessenger through its constructor |
using System;
namespace ConsoleAppNet
{
class Program
{
public static void Main()
{
try
{
Notification emailNotification = new Notification(new Email());
Notification smsNotification = new Notification(new SMS());
emailNotification.DoNotify();
smsNotification.DoNotify();
Console.ReadKey();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
public interface IMessenger
{
void SendMessage();
}
public class Email : IMessenger
{
public void SendMessage()
{
Console.WriteLine("Email Sent");
}
}
public class SMS : IMessenger
{
public void SendMessage()
{
Console.WriteLine("SMS Sent");
}
}
public class Notification
{
private IMessenger _messenger;
public Notification(IMessenger messenger)
{
_messenger = messenger;
}
public void DoNotify()
{
_messenger.SendMessage();
}
}
}
}[!TIP] The
Notificationclass does not know (or care) whether it is sending an email or an SMS. It only depends on theIMessengerabstraction, which is injected through the constructor.
I have followed many articles while writing this guide, but among them the following were really helpful. Those articles helped me a lot and encouraged me to write this article based on my understanding.
SOLID principles in C#:
SOLID principles in JavaScript:
- S.O.L.I.D: The First 5 Principles of Object-Oriented Design with JavaScript
- S.O.L.I.D: The First 5 Principles of Object-Oriented Design with JavaScript
- S.O.L.I.D: The First 5 Principles of Object-Oriented Design
- Implementing SOLID and the Onion Architecture in Node.js with TypeScript and InversifyJS
Md. Saiful Islam Microsoft Certified Solutions Developer (MCSD) – Programming in C#
GitHub: @saifaustcse LinkedIn: Md. Saiful Islam
If you find this guide useful, please give ⭐. Your support is appreciated!










