Introduction
Most explanations of Object-Oriented Programming (OOP) rely on examples such as Animal/Dog or Shape/Circle. These examples are useful for learning syntax, but they do not always demonstrate why OOP concepts matter in production applications.
Enterprise HR and Payroll systems provide a more realistic example. Consider a multi-level leave approval workflow where employees submit requests, managers approve them, and the application must protect the request's state throughout the process.
This scenario provides a practical way to understand five core OOP concepts in C#:
Class and Object
Abstraction
Encapsulation
Inheritance
Polymorphism
It also helps clarify the difference between an abstract class and an interface.
Class and Object: Definition vs. Real Data
In an HR or Payroll system, a Leave Request represents an actual business record submitted by an employee.
A class defines the structure and behavior that every leave request can have:
class LeaveRequest
{
public string EmpID;
public string LeaveRequestID;
public DateTime FromDate;
public DateTime ToDate;
public string Reason;
public int Status;
}
The class is a blueprint. It does not represent one particular employee's request.
An object is a specific instance of that class containing actual data:
LeaveRequest request = new LeaveRequest
{
EmpID = "1023",
LeaveRequestID = "LR1001",
FromDate = new DateTime(2026, 9, 20),
ToDate = new DateTime(2026, 9, 22),
Reason = "Personal",
Status = 0
};
There may be one LeaveRequest class definition in the application, but thousands of LeaveRequest objects can exist at runtime.
The distinction is simple:
Class → Defines what a leave request is.
Object → Represents one actual leave request.
This same relationship appears throughout enterprise applications: an Employee class can produce many employee objects, an Invoice class can produce many invoices, and a LeaveRequest class can produce many leave requests.
Abstraction: Hiding Implementation Complexity
When an employee submits a leave request, the application may perform many operations behind the scenes:
Validate the request
Check employee eligibility
Open a database connection
Execute a stored procedure or database command
Commit the transaction
Roll back if something fails
Handle exceptions
Return the result to the application
The employee or calling code does not need to understand all of these implementation details.
Instead, the application can expose a simple operation:
public bool SubmitLeaveRequest(LeaveRequest request)
{
// Validation
// Database operation
// Transaction handling
// Exception handling
return true;
}
The caller only needs to know that it can call SubmitLeaveRequest() and receive a result.
This is Abstraction: exposing the functionality that a caller needs while hiding implementation details that are not relevant to the caller.
For example:
bool submitted = service.SubmitLeaveRequest(request);
The calling code does not need to know whether the service uses Entity Framework Core, ADO.NET, a stored procedure, or another persistence mechanism.
The implementation can change without requiring every caller to understand those internal details.
Encapsulation: Protecting Business-Critical State
Encapsulation becomes particularly important in an approval workflow.
Suppose a leave request moves through several approval levels. The application must prevent arbitrary code from changing its status.
If Status is publicly writable:
request.Status = 1;
any part of the application could potentially mark the request as approved without completing the required workflow.
In an HR or Payroll system, this can create downstream problems because approval status may influence attendance processing or payroll calculations.
Instead, the object should control how its state changes:
class LeaveRequest
{
private int status;
public int Status
{
get { return status; }
private set { status = value; }
}
public void ApproveLevel(int currentSeqNo, int nextSeqNo)
{
if (currentSeqNo == nextSeqNo)
{
Status = 1;
}
}
}
The important part is private set.
Other classes can read the status:
int status = request.Status;
But they cannot directly change it:
request.Status = 1; // Not allowed
The request itself controls the state transition.
This is Encapsulation: keeping an object's internal state protected and allowing changes through controlled operations.
In a production application, the approval method would normally contain more comprehensive workflow rules, such as validating the current approval level, checking the approver's authorization, recording the approval event, and moving the request to the next state.
Inheritance: Sharing Common Request Behavior
Enterprise applications commonly contain several types of requests.
For example:
Leave Request
Advance Request
Reimbursement Request
Travel Request
Many of these requests share common information such as employee ID, submission date, and workflow state.
Instead of duplicating that structure in every class, common functionality can be placed in a base class.
abstract class BaseRequest
{
public string EmpID { get; set; }
public int Status { get; protected set; }
public DateTime SubmittedDate { get; set; }
public void ApproveLevel(int currentSeqNo, int nextSeqNo)
{
if (currentSeqNo == nextSeqNo)
{
Status = 1;
}
}
}
class LeaveRequest : BaseRequest
{
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }
public string Reason { get; set; }
}
class AdvanceRequest : BaseRequest
{
public decimal AdvanceAmount { get; set; }
public string Purpose { get; set; }
}
Join the conversation! Your thoughts help the community grow.