DEV Community

Dhana
Dhana

Posted on

OOP Concepts Explained Through a Real Payroll System, Not Animals and Shapes

Most OOP tutorials teach Class, Object, Abstraction, Encapsulation, Inheritance, and Polymorphism using Animal/Dog or Shape/Circle examples. They're fine for a first pass, but they rarely translate to how these concepts actually show up in real enterprise applications. This article walks through all four OOP pillars using a real, common enterprise scenario: a Payroll/HRMS leave-request and multi-level approval workflow.

Class vs Object: The Blueprint vs the Real Record

In a Payroll/HRMS system, an "Employee" or a "Leave Request" isn't just an abstract idea — it's a real record with real fields, submitted by real people.

A Class is the blueprint — the definition of what fields and behavior every Leave Request will have:

class LeaveRequest
{
    public string EmpID;
    public string LeaveRequestID;
    public DateTime FromDate;
    public DateTime ToDate;
    public string Reason;
    public int Status;
}
Enter fullscreen mode Exit fullscreen mode

An Object is one actual instance of that blueprint, filled with real data — a specific employee's specific leave request:

LeaveRequest req1 = new LeaveRequest 
{ 
    EmpID = "1023", 
    FromDate = new DateTime(2026, 9, 20), 
    ToDate = new DateTime(2026, 9, 22), 
    Reason = "Personal", 
    Status = 0 
};
Enter fullscreen mode Exit fullscreen mode

The Class exists once, in code. Objects exist many times — one for every leave request submitted across the organization, each with its own real values.

Abstraction: Hiding the Machinery Behind "Submit"

When an employee clicks "Submit" on a leave request, they don't see — and don't need to see — everything happening behind that click: parameter type validation, opening the database connection, executing the stored procedure, committing the transaction if everything succeeds, rolling back if something fails, and handling any errors along the way.

All the employee sees is a success message. That's Abstraction: exposing only what's necessary to the caller, while hiding the internal complexity.

public bool SubmitLeaveRequest(LeaveRequest request)
{
    // Hidden internally: connection handling, parameter checks,
    // commit/rollback logic, error handling
    return true; // caller just sees success or failure
}
Enter fullscreen mode Exit fullscreen mode

The calling code — whether it's a UI button or another service — doesn't need to know how the request gets saved, only that it did.

Encapsulation: Protecting Status From Being Changed Carelessly

A Leave Request's Status field shouldn't be something any part of the application can set directly. In a real approval workflow, a request typically moves through multiple levels — each level compares a workflow sequence number (wfSeqSlNo) against the next expected value (nextWfSeqSlNo); only when they match does the request actually progress toward "Approved."

If any code could bypass that check and set Status directly, a request could end up marked "Approved" without actually completing all required approval levels — a real compliance and payroll accuracy risk, not just a coding inconvenience.

Encapsulation protects against this by restricting direct access and only allowing changes through controlled logic:

class LeaveRequest
{
    private int status;

    public int Status
    {
        get { return status; }
        private set { status = value; }
    }

    public void ApproveLevel(int currentWfSeqSlNo, int nextWfSeqSlNo)
    {
        status = (currentWfSeqSlNo == nextWfSeqSlNo) ? 1 : 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, Status can only change through ApproveLevel(), which enforces the actual business rule — no code path can quietly skip the approval chain.

Inheritance: Sharing Common Fields Across Request Types

A Payroll/HRMS system rarely has just one kind of request. Alongside Leave Requests, there are often Advance Requests, Reimbursement Requests, and others — and most of them share the same underlying fields: an employee ID, a status, a submission date, and the same multi-level approval logic.

Rather than duplicating that logic in every request class, Inheritance lets you define it once, in a shared base class:

class BaseRequest
{
    public string EmpID;
    public int Status;
    public DateTime SubmittedDate;

    public void ApproveLevel(int currentWfSeqSlNo, int nextWfSeqSlNo)
    {
        Status = (currentWfSeqSlNo == nextWfSeqSlNo) ? 1 : 0;
    }
}

class LeaveRequest : BaseRequest
{
    public DateTime FromDate;
    public DateTime ToDate;
    public string Reason;
}

class AdvanceRequest : BaseRequest
{
    public decimal AdvanceAmount;
    public string Purpose;
}
Enter fullscreen mode Exit fullscreen mode

Both LeaveRequest and AdvanceRequest automatically get EmpID, Status, SubmittedDate, and the approval logic — without rewriting it. Each class only needs to define what's actually different about it.

Polymorphism: Each Request Type Handling Its Own Notification

Once a request is approved, the notification message should differ by type — a Leave approval message looks different from an Advance approval message. Polymorphism allows each child class to override a shared method with its own specific behavior, while still being treated as the same general type elsewhere in the code:

class BaseRequest
{
    public virtual string GetApprovalMessage()
    {
        return "Your request has been approved.";
    }
}

class LeaveRequest : BaseRequest
{
    public DateTime FromDate;
    public DateTime ToDate;

    public override string GetApprovalMessage()
    {
        return $"Your leave from {FromDate:d} to {ToDate:d} is approved.";
    }
}

class AdvanceRequest : BaseRequest
{
    public decimal AdvanceAmount;

    public override string GetApprovalMessage()
    {
        return $"Your advance of {AdvanceAmount} is approved and will reflect in next payroll.";
    }
}
Enter fullscreen mode Exit fullscreen mode

The practical benefit shows up when handling a mixed collection of requests — a list containing both Leave and Advance requests, for example. Calling GetApprovalMessage() on each one automatically returns the correct message for its actual type, without writing manual if/else checks to figure out what kind of request it is first.

Interface vs Abstract Class: Two Ways to Enforce a Contract

Once you have a shared BaseRequest class, a natural question comes up: should every request type be forced to have its own GetApprovalMessage(), and if so, how strict should that requirement be?

BaseRequest above is an abstract class — it already provides real, working code (EmpID, Status, ApproveLevel()), while still requiring child classes to implement their own version of specific methods. It gives you something and asks for something in return.

An interface goes further: it provides zero implementation, only a contract.

interface IApprovalNotifiable
{
    string GetApprovalMessage();
}
Enter fullscreen mode Exit fullscreen mode

Any class implementing IApprovalNotifiable is guaranteed to have a GetApprovalMessage() method — but gets no code for free. Compare that to BaseRequest, which hands LeaveRequest and AdvanceRequest real, working fields and methods before they write a single line themselves.

The practical distinction: use an abstract class when related types share meaningful common code worth writing once (like the approval logic every request type needs identically). Use an interface when you only need to guarantee that a capability exists — regardless of how differently each class chooses to implement it — with no shared code involved at all.

Why This Framing Helps

Generic Animal/Shape examples explain the mechanics of OOP well enough, but they don't show why any of it matters in a real system. Framed around an actual enterprise workflow — with real fields like EmpID, wfSeqSlNo, and real business risk behind getting Status wrong — these same five concepts stop being abstract syntax rules and start looking like decisions with real consequences: fewer duplicated classes, safer status transitions, and cleaner notification logic across multiple request types.

If you're working on any enterprise system with multi-step approval workflows — payroll, HR, procurement, or similar — these same patterns are very likely already present in your codebase, whether or not they've been named explicitly as Class, Abstraction, Encapsulation, Inheritance, and Polymorphism.

Top comments (0)