C#  

Understanding SOLID Principles with simple Console App in .NET

What is Principles in Software Development?

Principles in software development are like the compass that guides developers to build clean, maintainable, and efficient software. They’re not hard rules but rather best practices that help teams avoid technical debt, reduce bugs, and make the code easier to understand and evolve over time.

Think of Software Like a Library

Imagine we are building a public library. Over time, more books are added (like new features in our app), some books get moved, and sometimes we have to change the building layout (like updating code). If we don’t follow good rules and structure, the place becomes a mess.

❌  Without Good Practices

If books are placed randomly, some have no labels, and everyone follows their own way:

  • It’s hard to find what we need—just like messy code is hard to fix.
  • New workers (new developers) don’t know how things are organized.
  • Changing one shelf makes books fall elsewhere—just like how changing one line of code might break something else.

✅  With Good Practices

Now imagine everything is neat and organized:

  • Books are placed in the right section = each part of our code does one job.
  • No duplicate copies unless needed = don't repeat code.
  • Clear signs = easy-to-read, well-documented code.
  • Only build shelves when needed = don’t add extra features unless they’re really required.
  • Shelves can be adjusted easily = code can be changed or upgraded without breaking other parts.

Now our library runs smoothly, and adding new books or helping visitors is easy. That’s what good software principles do—they keep everything simple, clean, and easy to grow.

What Is SOLID?

SOLID is an acronym for five fundamental principles of object-oriented programming that help developers create software that is Maintainable, Scalable and Testable.

Each letter stands for a principle:

  • S – Single Responsibility Principle
  • O – Open/Closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

Who Invented It?

The term SOLID was coined by Michael Feathers, but it’s based on design principles introduced by Robert C. Martin (aka “Uncle Bob”), a legendary figure in the world of clean coding and software craftsmanship.

Why Use SOLID?

Without these principles, code becomes:

  • Hard to update (small change breaks many things)
  • Difficult to test
  • Painful to extend

SOLID makes code cleaner and more adaptable—so it works well not just today but also next year.

Let's we explore the SOLID principles in the .NET console app with layered architecture.

What Is Layered Architecture?

Layered Architecture is one of the most commonly used architectural styles. It divides a software system into logical layers, where each layer has a specific job and communicates only with adjacent layers.

Common Layers (Like in our Example Console App):

Layer Responsibility Example
Presentation Layer Takes input/output from users Program.cs
Business Logic Layer Core processing, rules, calculations MarkService.cs
Model/Entity Layer Data structure definitions Student.cs, Marks.cs
Abstraction Layer Interfaces / contracts IMarkService.cs

Why Use Layered Architecture?

  • Separation of Concerns: You can modify UI without touching business logic.
  • Easier Maintenance: Change in one layer doesn’t break others.
  • Reusability: Service or model layers can be reused in different apps (console/web/mobile).
  • Testability: You can test logic independently from UI.
  • Scalability: Easy to add new features or integrations layer-by-layer.

So, let's start with SOLID principles using a simple student management console app with layered architecture!

1. Single Responsibility Principle (SRP)

Rule

A class should have only one reason to change. That means each class handles one job, one responsibility.

In the real world

In a school, the Exam Section handles marks, the Admin Office handles student records, and the Result Department publishes the results. Each department does just one job.

1. Models/Student.cs

namespace Student_Management.Models
{
    public class Student
    {
        public string RegNo { get; set; }
        public string Name { get; set; }
        public Marks Marks { get; set; }
    }
}

2. Models/Marks.cs

namespace Student_Management.Models
{
    public class Marks
    {
        public int Tamil { get; set; }
        public int English { get; set; }
        public int Maths { get; set; }
        public int Science { get; set; }
        public int Social { get; set; }

        public int Total => Tamil + English + Maths + Science + Social;

        public string Result => (Tamil >= 35 && English >= 35 && Maths >= 35 && Science >= 35 && Social >= 35)
                                ? "Pass" : "Fail";
    }
}

Explanation

  • Follows Single Responsibility Principle – This classes are take only one responsible for storing student data another one storing student marks with calculate total and result.
  • Real World: Think of a student record in a school database.

Each class knows only what it needs to know. This separation means we can change how a result is printed without touching student or marks logic.

2. Open/Closed Principle (OCP)

Rule

Software should be open for extension but closed for modification. We can add new behavior without modifying existing code.

In the real world

Suppose the school decides to add result email notifications in addition to console printing. Instead of changing the existing report system, we plug in a new mechanism.

3. Interfaces/IMarkService.cs

public interface IMarkService
{
    void ShowResult(Student student);
}

👇 Existing Implementation: MarkService.cs

using Student_Management.Interfaces;
using Student_Management.Models;

namespace Student_Management.Services
{
    public class MarkService : IMarkService
    {
        public void ShowResult(Student student)
        {
            Console.WriteLine("\n----- STUDENT RESULT -----");
            Console.WriteLine($"RegNo : {student.RegNo}");
            Console.WriteLine($"Name  : {student.Name}");
            Console.WriteLine("\nMarks:");
            Console.WriteLine($"Tamil   : {student.Marks.Tamil}");
            Console.WriteLine($"English : {student.Marks.English}");
            Console.WriteLine($"Maths   : {student.Marks.Maths}");
            Console.WriteLine($"Science : {student.Marks.Science}");
            Console.WriteLine($"Social  : {student.Marks.Social}");
            Console.WriteLine("--------------------------");
            Console.WriteLine($"Total Marks : {student.Marks.Total}");
            Console.WriteLine($"Result      : {student.Marks.Result}");
            Console.WriteLine("--------------------------");
        }
    }
}

🔁 Tomorrow’s extension:

public class EmailResultService : IMarkService
{
    public void ShowResult(Student student)
    {
        // Email sending logic instead of console
    }
}

We can add new ways to show results (email, SMS, file export) without modifying the original MarkService. This protects existing functionality from accidental bugs.

3. Liskov Substitution Principle (LSP)

Rule

Subtypes should be substitutable for their base types without breaking the program.

In the real world

If the headmaster replaces the report team with another team that does the same job correctly, the school system shouldn't collapse.

Code Example from Program.cs

using Student_Management.Models;
using Student_Management.Interfaces;
using Student_Management.Services;

Console.Write("Enter Student RegNo: ");
string regNo = Console.ReadLine();

Console.Write("Enter Student Name: ");
string name = Console.ReadLine();

Console.WriteLine("Enter Marks out of 100:");

Console.Write("Tamil: ");
int tamil = Convert.ToInt32(Console.ReadLine());

Console.Write("English: ");
int english = Convert.ToInt32(Console.ReadLine());

Console.Write("Maths: ");
int maths = Convert.ToInt32(Console.ReadLine());

Console.Write("Science: ");
int science = Convert.ToInt32(Console.ReadLine());

Console.Write("Social: ");
int social = Convert.ToInt32(Console.ReadLine());

Student student = new Student
{
    RegNo = regNo,
    Name = name,
    Marks = new Marks
    {
        Tamil = tamil,
        English = english,
        Maths = maths,
        Science = science,
        Social = social
    }
};

IMarkService markService = new MarkService();
markService.ShowResult(student);

Suppose we replace MarkService like this:

IMarkService markService = new EmailResultService();

Even though the implementation changed, the behavior remains valid and the contract (interface) is respected.

We are treating both MarkService and any future implementation equally through IMarkService. That’s clean substitution with zero side effects.

4. Interface Segregation Principle (ISP)

Rule

Interfaces should be specific to what clients need. Don't force classes to implement methods they don't use.

In the real world

The staff printing the results shouldn’t be forced to also implement grading or emailing. Let each team focus.

Example: IMarkService.cs

public interface IMarkService
{
    void ShowResult(Student student);
}

We kept our interface very focused—just one method. If we had added unrelated operations (like SaveToDatabase() or EmailParent()), then every class would have been forced to implement them—even if they don’t use them.

By designing our interface with just enough behavior, we reduce bloating and keep each service class lean and focused.

5. Dependency Inversion Principle (DIP)

Rule

High-level modules should not depend on low-level modules. Both should depend on abstractions.

In the real world

The principal doesn’t care if the report comes from a printer, email, or mobile app—just that it follows the result protocol.

Example

IMarkService markService = new MarkService();
markService.ShowResult(student);
  • Program.cs (high-level app logic) depends only on the interface, not on the specific MarkService class.
  • This means you could inject a different class later without touching your console logic.

By depending on IMarkService, not MarkService, we invert the dependency chain and make our system more modular and testable.

How Layered Architecture Works with SOLID

Let’s tie this back to the SOLID principles in our app:

Single Responsibility

Each layer handles a single concern:

  • Models = data

  • Service = logic

  • Program = interface This maps directly to SRP.

Open/Closed Principle

We can add a new result display type (e.g., SMS) without changing existing logic. That's OCP in action—enhance the service layer, don’t rewrite it.

Liskov Substitution

Any class that implements IMarkService can be swapped without breaking the app. That’s LSP and thanks to layering, this works cleanly.

Interface Segregation

We defined a small, focused interface with ShowResult(Student). Each implementation layer uses only what it needs. That’s ISP and layered design supports this division.

Dependency Inversion

Our app logic (Program.cs) uses interfaces, not concrete services. That’s DIP—and layered architecture enforces this kind of dependency direction.

Output of the Console Application

Conclusion

The SOLID principles are more than just theoretical concepts — they are practical guidelines that help developers write clean, maintainable, and scalable code. By applying these five principles, we can create software that is easier to understand, test, and modify over time. Whether you’re working on a small project or a large-scale enterprise application, SOLID lays the foundation for long-term success and adaptability. Embracing SOLID isn’t just about writing better code — it's about building better software.