Introduction

Object-Oriented Programming (OOP) is a programming paradigm widely used in C# and .NET development. Instead of organizing an application only around functions and procedures, OOP organizes code around objects that combine data and behavior.

For beginners, the four fundamental OOP concepts are:

These concepts are easier to understand when they are connected to a practical application rather than treated as separate definitions.

In this article, we will use a simple banking application to understand each concept with C# examples and see how the concepts work together.

What Is Object-Oriented Programming?

Object-Oriented Programming focuses on objects that contain both state and behavior.

For example, a bank account can have:

State

Behavior

In C#, a class can represent the structure and behavior of such an object.

public class BankAccount
{
    public string AccountNumber { get; set; }
    public string AccountHolder { get; set; }
    public decimal Balance { get; set; }

    public void Deposit(decimal amount)
    {
        Balance += amount;
    }
}

An object can then be created from the class:

BankAccount account = new BankAccount
{
    AccountNumber = "ACC1001",
    AccountHolder = "Rakesh",
    Balance = 5000
};

account.Deposit(1000);

Console.WriteLine(account.Balance);

Output:

6000

The class defines the structure and behavior, while the object represents an actual instance.

Why Use OOP?

OOP can help developers organize larger applications by separating responsibilities into meaningful classes and relationships.

Common benefits include:

However, OOP is not simply about creating as many classes as possible. Good object-oriented design also requires choosing appropriate responsibilities and relationships between objects.

The Four Pillars of OOP

The four commonly taught pillars are:

  1. Encapsulation

  2. Inheritance

  3. Polymorphism

  4. Abstraction

Let's examine each one using C#.

Encapsulation

Encapsulation means controlling how an object's internal state is accessed and modified.

Instead of allowing every part of an application to change a bank account's balance directly, the account can expose operations such as Deposit() and Withdraw().

Real-World Analogy: ATM

When using an ATM, you do not directly manipulate the bank's internal account records.

Instead, you interact with operations such as:

The banking system controls what happens internally.

The same idea can be applied to a C# class.

C# Example

Instead of exposing the balance as a freely writable property:

public decimal Balance { get; set; }

we can protect it:

public class BankAccount
{
    private decimal _balance;

    public decimal GetBalance()
    {
        return _balance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException(
                "Deposit amount must be greater than zero.");

        _balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException(
                "Withdrawal amount must be greater than zero.");

        if (amount > _balance)
            throw new InvalidOperationException(
                "Insufficient balance.");

        _balance -= amount;
    }
}

Now external code cannot directly modify _balance.

Instead:

BankAccount account = new BankAccount();

account.Deposit(5000);
account.Withdraw(1500);

Console.WriteLine(account.GetBalance());

Output:

3500

The class controls how the balance changes.

Why Encapsulation Matters

Without encapsulation, another part of the application could potentially do something like:

account.Balance = -50000;

That could violate the business rules of the banking system.

By keeping the field private and exposing controlled operations, the class becomes responsible for protecting its own state.

Inheritance

Inheritance allows a class to derive from another class and reuse accessible members from the base class.

For example, different types of bank accounts may share common behavior.

We can create a base class:

public class BankAccount
{
    public string AccountNumber { get; set; } = string.Empty;

    public decimal Balance { get; protected set; }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException(
                "Amount must be greater than zero.");

        Balance += amount;
    }
}

A savings account can inherit from it:

public class SavingsAccount : BankAccount
{
    public decimal InterestRate { get; set; }
}

A current account can also inherit from it:

public class CurrentAccount : BankAccount
{
    public decimal OverdraftLimit { get; set; }
}

Now both derived classes can use the common Deposit() behavior.

SavingsAccount savingsAccount = new SavingsAccount();

savingsAccount.Deposit(5000);

Console.WriteLine(savingsAccount.Balance);

Output:

5000

Real-World Analogy

Think about different types of vehicles.

A car and a bike are both vehicles. They may share common behavior such as starting and stopping while having their own specialized behavior.

The same relationship can be represented in C#:

             Vehicle
              /   \
             /     \
           Car     Bike

Important Design Consideration

Inheritance should represent a genuine is-a relationship.

For example:

SavingsAccount is a BankAccount

makes sense.

But:

BankAccount is a Database

does not represent an appropriate inheritance relationship. Composition would be more appropriate in such a case.

Polymorphism

Polymorphism means that the same interface or operation can have different implementations.

In C#, polymorphism commonly appears through:

  1. Method overloading

  2. Method overriding

  3. Interface-based programming

Compile-Time Polymorphism: Method Overloading

Method overloading allows multiple methods to have the same name but different parameter lists.

public class PaymentService
{
    public void ProcessPayment(decimal amount)
    {
        Console.WriteLine(
            $"Processing payment of {amount}");
    }

    public void ProcessPayment(
        decimal amount,
        string currency)
    {
        Console.WriteLine(
            $"Processing {amount} {currency}");
    }
}

The compiler determines which method should be called based on the arguments.

PaymentService service = new PaymentService();

service.ProcessPayment(1000);
service.ProcessPayment(1000, "USD");

Output:

Processing payment of 1000
Processing 1000 USD

Runtime Polymorphism: Method Overriding

Runtime polymorphism allows a derived class to provide its own implementation of a base-class method.

Consider different account types calculating interest differently.

public class BankAccount
{
    public decimal Balance { get; set; }

    public virtual decimal CalculateInterest()
    {
        return 0;
    }
}

A savings account can override the method:

public class SavingsAccount : BankAccount
{
    public override decimal CalculateInterest()
    {
        return Balance * 0.04m;
    }
}

A premium account can provide another implementation:

public class PremiumAccount : BankAccount
{
    public override decimal CalculateInterest()
    {
        return Balance * 0.06m;
    }
}

Now the same method call can produce different results:

BankAccount account1 = new SavingsAccount
{
    Balance = 10000
};

BankAccount account2 = new PremiumAccount
{
    Balance = 10000
};

Console.WriteLine(account1.CalculateInterest());
Console.WriteLine(account2.CalculateInterest());

Output:

400.00
600.00

The variables have the same base type, but the runtime invokes the appropriate overridden implementation.

This is runtime polymorphism.

Abstraction

Abstraction means exposing the essential behavior while hiding implementation details.

A common way to implement abstraction in C# is through abstract classes or interfaces.

Real-World Analogy: ATM

When you withdraw money from an ATM, you know what operation you want to perform:

Withdraw Money

You do not need to know the internal implementation involving banking systems, transaction processing, validation, and database operations.

The interface exposed to the user is simpler than the implementation behind it.

C# Example with an Abstract Class

public abstract class PaymentMethod
{
    public abstract void Pay(decimal amount);
}

Different payment methods can implement their own behavior.

public class CreditCardPayment : PaymentMethod
{
    public override void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Paid {amount} using credit card.");
    }
}

Another implementation:

public class UpiPayment : PaymentMethod
{
    public override void Pay(decimal amount)
    {
        Console.WriteLine(
            $"Paid {amount} using UPI.");
    }
}

The caller only needs to work with PaymentMethod:

PaymentMethod payment =
    new UpiPayment();

payment.Pay(1500);

Output:

Paid 1500 using UPI.

The caller does not need to know the internal details of the UPI payment implementation.

Abstraction vs Encapsulation

These two concepts are often confused.

Encapsulation

Abstraction

Controls access to internal state and implementation

Exposes essential behavior while hiding unnecessary implementation details

Commonly uses access modifiers

Commonly uses interfaces and abstract classes

Protects object state

Simplifies how functionality is consumed

Focuses on how access is controlled

Focuses on what functionality is exposed

A simple way to remember the difference is:

Encapsulation → How do I protect the internals?

Abstraction   → What should the caller need to know?

In real applications, the two concepts often work together.

Combining the Four OOP Concepts

The four principles are not isolated features.

A realistic application may use all of them together.

For example, consider a banking application:

                    BankAccount
                         |
             +-----------+-----------+
             |                       |
       SavingsAccount          CurrentAccount
             |                       |
       Interest Rules            Overdraft Rules

Encapsulation protects account state.

Inheritance allows specialized account types to reuse common functionality.

Polymorphism allows different account types to implement behavior differently.

Abstraction allows other parts of the application to work with a general account or service contract without depending on implementation details.

Complete Example

The following example combines several OOP concepts:

using System;

public abstract class BankAccount
{
    private decimal _balance;

    public string AccountNumber { get; }

    protected BankAccount(
        string accountNumber,
        decimal initialBalance)
    {
        AccountNumber = accountNumber;
        _balance = initialBalance;
    }

    public decimal GetBalance()
    {
        return _balance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException(
                "Amount must be greater than zero.");

        _balance += amount;
    }

    public abstract decimal CalculateInterest();
}

public class SavingsAccount : BankAccount
{
    public SavingsAccount(
        string accountNumber,
        decimal initialBalance)
        : base(accountNumber, initialBalance)
    {
    }

    public override decimal CalculateInterest()
    {
        return GetBalance() * 0.04m;
    }
}

public class PremiumAccount : BankAccount
{
    public PremiumAccount(
        string accountNumber,
        decimal initialBalance)
        : base(accountNumber, initialBalance)
    {
    }

    public override decimal CalculateInterest()
    {
        return GetBalance() * 0.06m;
    }
}

public class Program
{
    public static void Main()
    {
        BankAccount savings =
            new SavingsAccount("SAV001", 10000);

        BankAccount premium =
            new PremiumAccount("PRE001", 10000);

        savings.Deposit(2000);

        Console.WriteLine(
            $"Savings Balance: {savings.GetBalance()}");

        Console.WriteLine(
            $"Savings Interest: {savings.CalculateInterest()}");

        Console.WriteLine(
            $"Premium Interest: {premium.CalculateInterest()}");
    }
}

Output

Savings Balance: 12000
Savings Interest: 480.00
Premium Interest: 600.00

This single example demonstrates several concepts:

OOP in Real-World Applications

OOP principles are commonly used in many types of applications.

Banking Applications

Encapsulation can protect account and transaction state, while polymorphism can represent different account or payment behaviors.

E-Commerce Applications

Products, orders, customers, carts, and payment methods can be modeled as domain objects.

Different payment providers can implement a common payment interface:

IPayment
   |
   +-- CreditCardPayment
   +-- UpiPayment
   +-- PayPalPayment

Gaming Applications

A game may contain a common character abstraction with specialized player, enemy, and non-player character implementations.

Enterprise Applications

Interfaces and abstractions can separate business logic from infrastructure such as databases, messaging systems, and external services.

Common OOP Interview Questions

What are the four pillars of OOP?

They are:

What is the difference between overloading and overriding?

Overloading defines multiple methods with the same name but different parameter lists.

Overriding allows a derived class to replace a virtual or abstract base-class implementation.

Can a static method be overridden in C#?

No. Static methods belong to the type rather than an object instance and cannot participate in runtime overriding.

A derived class can hide a static member, but that is different from overriding it.

What is the difference between an abstract class and an interface?

An abstract class can contain state, constructors, implemented members, and abstract members.

An interface primarily defines a contract that implementing types agree to provide. Modern C# interfaces can also contain certain default implementations and static members, so the distinction is broader than simply "interfaces contain only methods."

Does inheritance always improve code reuse?

No.

Inheritance should be used when the relationship between the types makes sense. Otherwise, composition is often a better design choice.

Common Mistakes When Learning OOP

Treating OOP as Only Four Definitions

Memorizing the four pillars is not enough. Developers should understand when each concept improves a design.

Overusing Inheritance

Not every reusable component should become a base class.

Composition and dependency injection are often better alternatives.

Exposing Internal State

Making every field publicly writable can make it difficult to enforce business rules.

Use appropriate access modifiers and controlled operations.

Creating Classes Without Clear Responsibilities

A class should have a meaningful responsibility. Simply converting every noun in a requirement into a class can result in unnecessary complexity.

Confusing Abstraction with Encapsulation

They are related but solve different problems. Encapsulation controls access to internals, while abstraction simplifies what consumers need to interact with.

Key Takeaway

OOP provides a way to structure software around objects, their responsibilities, and their relationships.

The four fundamental concepts can be summarized as:

Encapsulation
     ↓
Protect the object's internal state

Inheritance
     ↓
Create specialized types from a base type

Polymorphism
     ↓
Allow different implementations of common behavior

Abstraction
     ↓
Expose essential behavior and hide implementation details

The real value of OOP comes from applying these concepts appropriately rather than using them simply because they are available.

Conclusion

Object-Oriented Programming is an important foundation for C# and .NET development. Encapsulation, inheritance, polymorphism, and abstraction provide different mechanisms for organizing code and managing complexity.

In the banking example, encapsulation protected the account balance, inheritance allowed specialized account types to share common behavior, polymorphism enabled different interest calculations, and abstraction provided a common model for working with different account implementations.

For beginners preparing for interviews, understanding the definitions is useful, but being able to explain why a particular design uses encapsulation, inheritance, polymorphism, or abstraction is much more valuable.

For the final C# Corner submission, the author should also add screenshots from their own execution environment and, if requested by the editor, attach the actual working POC source code. Any personal observations or real-world experiences should be added by the author in their own words rather than being fabricated.