Infoveave Engineering Team··12 min read
Episode 01 · Software Engineering, Taught Right

OOP Four Pillars: What Tutorials Never Tell You

You've used classes for years. You can define all four pillars. And yet something still feels off when you design a system from scratch. Here's why.

You learned OOP the same way everyone did. A Car class. An Animal with a Dog subclass. Four pillars, four definitions, four boxes ticked.
And then you joined a real codebase, and nothing felt like the tutorial.
The reason is simple: most OOP tutorials teach you the vocabulary, not the reason the vocabulary exists. They answer "what is encapsulation?" but they never ask "what breaks without it?"
OOP isn't about objects. It's about what happens to data when nobody owns it.

The Mess That OOP Was Invented to Fix

Before classes, data was free. Anyone could touch it. No rules, no owners, no consequences — until production.
csharp
// No ownership — a structural problem, not a developer problem

public static decimal balance = 1000m;

public static void ProcessTransaction(decimal amount)
{
    balance += amount; // no validation, no rules
}

// written by a different dev, in a different file:
balance = -99999m; // compiles fine. ships to prod.
This compiles. It runs. It's a disaster.
The developer isn't the problem — the structure is. The data has no owner. Any file, any function, any junior dev at 2am can mutate it, and nothing stops them.
That's the exact problem OOP was designed to solve. Not "how to model the real world." Not "how to write blueprints." How to give data a responsible owner.

Pillar 01 — Encapsulation Is Ownership, Not Hiding

Encapsulation is the practice of bundling data and the methods that operate on that data together inside a single unit — a class — and restricting direct access to that data from the outside world.
Every tutorial stops there and says it means "hiding data." That's not wrong, it's just not the point. The point is: data should have one owner, and that owner enforces the rules.
csharp
// One owner. One door. One set of rules.

public class BankAccount
{
    private decimal _balance;

    public BankAccount(decimal initial)
    {
        if (initial < 0) throw new ArgumentException("Invalid.");
        _balance = initial;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Invalid deposit.");
        _balance += amount;
    }

    public decimal Balance => _balance; // read-only. look, don't touch.
}
_balance has one owner now. The rules live exactly where the data lives, and nobody sets it to -99999 — the compiler won't allow it. That's not hiding. That's responsibility. And that's a much bigger idea.
A detail most tutorials skip: private isn't magic security. The runtime doesn't hide private fields in some secret place — the field still exists plainly in memory. private is the compiler saying: Code outside this class is not allowed to touch this.

Pillar 02 — Abstraction Absorbs Complexity

Abstraction is the principle of exposing only what is necessary and hiding the implementation details behind a clean interface. You interact with what a thing does, not how it does it.
Tutorials say it means "show the essential, hide the detail." True, but there's a sharper way to say it: abstraction lets callers think less.
When someone calls account.Deposit(500), they don't know about the validation logic, the audit log entry, the rounding rules, or the fraud flag check. They don't need to. That complexity lives inside the method, contained and invisible to the outside. Good abstraction doesn't eliminate complexity — it contains it, so the rest of the system can stay clean while the internals evolve.
Here's where abstraction becomes concrete. Imagine a notification system. The naive version exposes every detail to every caller:
csharp
// No abstraction — caller must know everything

if (user.PrefersSms)
{
    var smsClient = new TwilioClient(apiKey, secret);
    smsClient.Messages.Create(user.Phone, from: "+1234", body: message);
}
else if (user.PrefersEmail)
{
    var smtp = new SmtpClient("smtp.example.com", 587);
    smtp.Send(new MailMessage("[email protected]", user.Email, "Alert", message));
}
// adding WhatsApp? Come back and add another branch.
The caller knows about Twilio. It knows about SMTP. It knows about every transport. Adding a new channel means reopening this code.
Abstraction removes all of that:
csharp
// One interface. Caller knows nothing about the transport.

public interface INotificationService
{
    Task SendAsync(string userId, string message);
}

// Usage — caller sees only this:
await _notifications.SendAsync(user.Id, "Your report is ready.");
The SmtpNotificationService, SmsNotificationService, and WhatsAppNotificationService all implement INotificationService. The caller never changes when a new transport is added. The implementation can change — a new provider, a different SDK, a rate limiter — without touching the code that uses it.
Where abstraction and encapsulation differ: Encapsulation controls who may touch data. Abstraction controls what a caller must know to use a component. They often work together — the notification service hides its transport credentials (encapsulation) and exposes only SendAsync (abstraction) — but they solve different problems.

Pillar 03 — Inheritance Is Specialisation, Not the Default

Inheritance is a mechanism that allows a class to derive from another class, inheriting its fields and methods while being able to extend or override them. It expresses an "is-a" relationship — a SavingsAccount is a BankAccount with one extra rule: it earns interest.
csharp
// Specialised form of a general type

public class SavingsAccount : BankAccount
{
    private decimal _rate;

    public SavingsAccount(decimal initial, decimal rate) : base(initial)
        => _rate = rate;

    public virtual decimal CalculateInterest() => Balance * _rate;
}
Same structure as a BankAccount, with extra behaviour added on top. That's the legitimate use of inheritance. But here's what most tutorials don't say: inheritance is a tool, not a default.
Deep inheritance chains — Account → BankAccount → SavingsAccount → PremiumSavingsAccount — become brittle fast. A change at the top breaks everything below. When in doubt, prefer composition: build behaviour by combining small pieces, not by chaining parents. Most tutorials never tell you that. Now you know.

Pillar 04 — Polymorphism Removes the Switchboard

Polymorphism means the same interface can produce different behaviour depending on the underlying type. A single method call resolves to different implementations at runtime, based on what object is actually behind it.
Without it, your codebase fills up with this:
csharp
// Every new type breaks this

if (account is SavingsAccount) CalculateSavingsInterest();
else if (account is CurrentAccount) CalculateCurrentInterest();
else if (account is FixedDeposit) CalculateFixedInterest();
// add a new account type → come back and add another branch
Every new type forces you to reopen old code. That's fragile. Polymorphism removes the switchboard entirely.
csharp
// Add new types without touching this loop — ever

public interface IInterestBearing
{
    decimal CalculateInterest();
}

foreach (var account in accounts)
    account.Deposit(account.CalculateInterest());
One interface. Many behaviours. Zero branching. Add a CryptoAccount tomorrow, implement IInterestBearing, and the loop above doesn't change — not one line. That's not a pattern. That's engineering leverage.

The Four Pillars Are Really One

PillarControls
EncapsulationWho may change data
AbstractionWhat complexity others must see
InheritanceHow structure is reused
PolymorphismHow behaviour varies
Same theme running through all four. Control. Not control as in rigid or bureaucratic — control as in intentional, deliberate, designed.
Most tutorials get this backwards. They present four separate concepts and ask you to memorise them. But they're not separate. They're four consequences of one underlying principle:
Protect integrity while allowing change.
That's what good software architecture is always trying to do. OOP is just one disciplined way of doing it.
ENCAPSULATIONProblem: nobody owns the dataAny code can change balance directly.Rules live nowhere. Bugs live everywhere.Tool: private fields + controlled methodsOne class owns the data. Rules live there.External code can only call what's exposed.ABSTRACTIONProblem: callers see too much complexityCallers know the algorithm, the DB schema,the HTTP headers. Every change breaks them.Tool: interfaces — expose what, hide howCaller depends on contract. Implementationcan change freely underneath.INHERITANCEProblem: duplicated structureSame fields and methods copied acrossmultiple types. One fix, many places.Tool: extend a base (use sparingly)is-a → inherit · has-a → compose insteadPOLYMORPHISMProblem: type-checking switchboardif (type == A) ... else if (type == B) ...Every new type = modify existing code.Tool: one interface, many implementationsNew type = new class. Existing code unchanged.Protect integritywhile allowing change

The Two Pairs People Confuse

Two pairs of pillars are routinely mixed up in interviews and in code reviews. Getting them clear matters.

Encapsulation vs Abstraction

Both involve "hiding something" — which is why they blur together.
  • Encapsulation hides data. The _balance field is private so nothing outside BankAccount can set it directly. The rule lives with the data.
  • Abstraction hides implementation. The INotificationService interface hides whether you're sending via SMTP, SMS, or Twilio. The caller doesn't know and doesn't need to know.
A class can encapsulate data without abstracting its interface (a public class with private fields). A class can provide a clean interface without encapsulating anything (a static utility class). They are independent properties that usually appear together in well-designed code.

Polymorphism vs Inheritance

Inheritance is a mechanism — one class deriving from another. Polymorphism is a behaviour — different types responding differently to the same call. You can have polymorphism without inheritance (interface-based polymorphism, which is almost always preferred in modern C#), and you can have inheritance without polymorphism (non-virtual methods in a derived class that don't override anything).
When people say "use polymorphism", they typically mean: define an interface, implement it in multiple classes, and write calling code that depends only on the interface. Whether those classes share a base class is a separate decision.

A Practical Design Checklist

Before committing a new class, run these four questions:
Encapsulation: Is every field that external code has no business touching marked private? Are the rules that govern it enforced inside the class, not by convention?
Abstraction: Could this class's caller do its job knowing only the method signatures and nothing about the implementation? If the answer is no — the interface is leaking.
Inheritance: Is this genuinely a specialised form of the base type, or am I inheriting to reuse code? If the latter, extract the shared code into a service and inject it instead.
Polymorphism: Are there if (x is TypeA) ... else if (x is TypeB) patterns in this codebase? Each one is a candidate for an interface and a polymorphic dispatch.
The four pillars are not a checklist to apply mechanically. They are the shape that emerges from asking the right questions about ownership, complexity, structure, and variance — every time you design a new component.

So next time you write a class — before you decide what's public and what's private — ask the real questions: Who owns this data? What rules protect it? What behaviour needs to vary?
Answer those well and the four pillars will emerge on their own. Not as theory. As consequences.
That's when OOP stops being a set of concepts you know, and starts being a way you think.

How We Use This at Infoveave

Infoveave's connector system is a live example of polymorphism done right. Every data source — Salesforce, SAP, PostgreSQL, Google Sheets, a REST API — implements the same connector interface. The data ingestion engine calls connector.fetch() and connector.schema() without knowing or caring what's on the other side. Adding a new connector means writing one new class, not touching the engine. That's the Open/Closed Principle as a working engineering constraint, not a textbook definition. The same pattern runs through the transformation layer, the quality rule engine, and the automation scheduler — each designed so that adding a new type extends the system rather than modifying it.

Explore the Platform

About the Authors

This article was written by the Infoveave Engineering Team — building Unified Data Platform, agentic BI, and enterprise analytics infrastructure. Infoveave (by Noesys Software) helps organisations unify data, automate business processes, and act faster with AI-powered insights.

Ready to see Infoveave in action?

Book a Demo
ISO 27001ISO 27017ISO 27701GDPRHIPAACCPAAICPACSR LogoCapterra Reviews — Infoveave

© 2026 Noesys Software Pvt Ltd

Infoveave® is a product of Noesys

All Rights Reserved