LearnAI ToolsCareerPractice BuildsPlayContact
JavaIntermediate~2 hours

Employee Payroll System

Calculate salaries for different employee types using interfaces and abstract classes.

InterfacesAbstractionArrays

Overview

Every company pays different kinds of employees in fundamentally different ways — a salaried employee earns the same amount every period, an hourly employee earns based on hours worked, and a commission-based employee earns a percentage of what they sell. What all three genuinely share is not a formula, only a promise: every one of them is "payable," meaning you can always ask it for a salary. Java's `interface` is built to express exactly that kind of promise, separately from any shared implementation.

By the end of this tutorial you will have a console payroll application built around a `Payable` interface, an `abstract` `Employee` class that implements it without fully satisfying it, and three concrete subclasses that each override `calculateSalary()` with their own formula. Every employee, regardless of type, is stored in a single fixed-size `Employee[]` array, and running payroll is one loop that calls `calculateSalary()` on each element — the array never needs to know or care which concrete class any given element actually is.

What You'll Build
  • A `Payable` interface declaring a single `calculateSalary()` method.
  • An abstract `Employee` class implementing `Payable` without providing a body for `calculateSalary()`.
  • A `SalariedEmployee` subclass paying a fixed monthly amount.
  • An `HourlyEmployee` subclass paying `hoursWorked * hourlyRate`, with overtime past 160 hours.
  • A `CommissionEmployee` subclass paying a base plus a percentage of sales.
  • A `Payroll` class managing a fixed-size `Employee[]` array and printing a full payroll report polymorphically.

Prerequisites

  • Interfaces — declaring a `interface` with an abstract method and `implements`-ing it in a class.
  • Abstraction — the difference between an `interface` (a pure contract) and an `abstract class` (a partial implementation).
  • Inheritance and polymorphism — `extends`, `@Override`, and calling an overridden method through a base reference.
  • Arrays — declaring a fixed-size `Employee[]`, storing elements by index, and iterating with a `for` loop.
  • Method basics — return types, parameters, and calling one method from another.

Project Structure

The program lives in a single file, `EmployeePayrollSystem.java`. `Payable` is a pure interface: it has no fields and no method bodies, only the signature `double calculateSalary();`, which is implicitly `public abstract`. `Employee` is declared `abstract class Employee implements Payable` — it satisfies the "is a class that can be paid" promise structurally by implementing `Payable`, but it deliberately does not provide a body for `calculateSalary()` itself, re-declaring it as `abstract`. Java allows this specific combination: an abstract class is allowed to leave an interface method unimplemented, as long as the class itself is marked `abstract`, pushing the actual obligation down to whichever concrete subclass extends it.

`Payroll` owns a fixed-size `Employee[]` array rather than a growable `ArrayList<Employee>` — the array size is decided once in the constructor, and `addEmployee()` fills the next open slot, which is a deliberate, simpler alternative chosen here specifically to practice array mechanics (tracking a `count` of filled slots, checking for a full array) that a `List` would otherwise hide from you.

Step 1: Define the Payable Interface

An interface method has no body and, as of any modern Java version, no visibility modifier is needed on it — every method declared in an interface is `public abstract` by default unless marked `default` or `static`. `Payable` says nothing about how a salary is computed, only that any class implementing it must be able to answer the question.

// A pure contract: any class that "implements Payable" is promising it can
// compute a salary, without saying anything about how. No fields, no bodies.
interface Payable {
double calculateSalary(); // Implicitly public and abstract; every implementer must define this
}

Step 2: Define an Abstract Employee Base Class

Every employee — regardless of pay structure — has an id and a name, so those live in `Employee` as `protected` fields that subclasses can read directly. `Employee implements Payable` but re-declares `calculateSalary()` as `abstract` rather than providing a formula, which is legal specifically because `Employee` itself is marked `abstract class` — the compiler defers the "you must implement this" obligation to whichever concrete subclass extends `Employee`.

abstract class Employee implements Payable {
protected int id; // protected, not private, so subclasses can read it directly
protected String name; // Employee's display name
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
// No calculateSalary() body here: Employee satisfies "implements Payable" structurally,
// but leaves the real obligation to SalariedEmployee, HourlyEmployee, and CommissionEmployee.
public void printPayslip() {
System.out.printf("ID: %-4d | Name: %-18s | Type: %-12s | Salary: %.2f%n",
id, name, getEmployeeType(), calculateSalary()); // Both calls dispatch to the real subclass at runtime
}
public abstract String getEmployeeType(); // A second abstract method, so payslips can label each employee's type
}

`printPayslip()` is itself never overridden by any subclass — it does not need to be. It already produces correct, type-specific output for any employee by calling the abstract `calculateSalary()` and `getEmployeeType()` internally, the same "small abstract methods power one larger shared method" pattern used by `Member.displayInfo()` in the Library Management System project.

Step 3: Derive SalariedEmployee and HourlyEmployee

`SalariedEmployee` is the simplest possible implementation: `calculateSalary()` just returns a fixed field. `HourlyEmployee` is more interesting — it multiplies `hoursWorked` by `hourlyRate` for the first 160 hours, then applies a 1.5x overtime multiplier to anything beyond that, all inside its own `calculateSalary()` override, completely invisible to any code that only knows it is holding an `Employee`.

class SalariedEmployee extends Employee {
private double monthlySalary; // Fixed amount paid every period, regardless of hours
public SalariedEmployee(int id, String name, double monthlySalary) {
super(id, name); // Forwards id and name to Employee's constructor
this.monthlySalary = monthlySalary;
}
@Override
public double calculateSalary() {
return monthlySalary; // No formula needed: salaried pay doesn't depend on hours or sales
}
@Override
public String getEmployeeType() {
return "Salaried";
}
}
class HourlyEmployee extends Employee {
private double hoursWorked; // Hours worked this pay period
private double hourlyRate; // Base pay per hour
public HourlyEmployee(int id, String name, double hoursWorked, double hourlyRate) {
super(id, name);
this.hoursWorked = hoursWorked;
this.hourlyRate = hourlyRate;
}
@Override
public double calculateSalary() {
double regularHours = Math.min(hoursWorked, 160); // Cap regular-rate hours at 160 per period
double overtimeHours = Math.max(0, hoursWorked - 160); // Anything past 160 counts as overtime
return (regularHours * hourlyRate) + (overtimeHours * hourlyRate * 1.5); // Overtime pays 1.5x
}
@Override
public String getEmployeeType() {
return "Hourly";
}
}
Example Usage

Click Run to see what this code prints.

Step 4: Derive a CommissionEmployee

A third subclass drives home that `calculateSalary()` can be computed however each type needs — `CommissionEmployee` combines a flat base pay with a percentage of sales, a formula that shares nothing structurally with either `SalariedEmployee` or `HourlyEmployee`, yet all three are equally valid `Employee` objects to any code that only calls the methods `Employee` and `Payable` promise.

class CommissionEmployee extends Employee {
private double baseSalary; // Guaranteed pay regardless of sales performance
private double salesAmount; // Total sales generated this period
private double commissionRate; // Fraction of sales paid as commission, e.g. 0.05 for 5%
public CommissionEmployee(int id, String name, double baseSalary, double salesAmount, double commissionRate) {
super(id, name);
this.baseSalary = baseSalary;
this.salesAmount = salesAmount;
this.commissionRate = commissionRate;
}
@Override
public double calculateSalary() {
return baseSalary + (salesAmount * commissionRate); // Base pay plus a cut of whatever they sold
}
@Override
public String getEmployeeType() {
return "Commission";
}
}

Step 5: Build the Payroll Class Around an Employee Array

`Payroll` allocates a fixed-size `Employee[]` in its constructor and tracks how many slots are actually filled with a `count` field, since a Java array (unlike an `ArrayList`) has no built-in notion of "how many elements are really in use" versus "how large the array is." `addEmployee()` checks `count` against the array's `.length` before writing, since writing past a full array would throw an `ArrayIndexOutOfBoundsException`.

class Payroll {
private Employee[] employees; // Fixed-size array; every element is an Employee reference, concrete type varies
private int count = 0; // How many slots are actually filled, since employees.length never shrinks or grows
public Payroll(int capacity) {
employees = new Employee[capacity]; // Allocates capacity slots, all initially null
}
public boolean addEmployee(Employee e) {
if (count >= employees.length) { // Guard against writing past the array's fixed size
System.out.println("Payroll is full; cannot add more employees.");
return false;
}
employees[count] = e; // Store at the next open slot
count++;
return true;
}
public void runPayroll() {
System.out.println("\n--- Payroll Report ---");
double total = 0;
for (int i = 0; i < count; i++) { // Only iterate over filled slots, not the whole array
employees[i].printPayslip(); // Polymorphic: dispatches to each employee's real calculateSalary()
total += employees[i].calculateSalary();
}
System.out.printf("Total payroll this period: %.2f%n", total);
}
public Employee findById(int id) {
for (int i = 0; i < count; i++) {
if (employees[i].getId() == id) {
return employees[i];
}
}
return null;
}
}

Step 6: Build the Menu Loop

`main()` creates a `Payroll` with a fixed capacity and loops over a menu that lets the user add any of the three employee types or run the full payroll report. Adding an employee is the one place the user's choice directly decides which concrete class gets constructed, while `runPayroll()` afterward treats every element of the array uniformly through its `Employee` type.

import java.util.Scanner;
public class EmployeePayrollSystem {
public static void main(String[] args) {
Payroll payroll = new Payroll(50); // Fixed capacity of 50 employees for this run
Scanner scanner = new Scanner(System.in);
int nextId = 1;
int choice;
do {
System.out.println("\n===== EMPLOYEE PAYROLL SYSTEM =====");
System.out.println("1. Add Salaried Employee");
System.out.println("2. Add Hourly Employee");
System.out.println("3. Add Commission Employee");
System.out.println("4. Run Payroll Report");
System.out.println("5. Find Employee by ID");
System.out.println("6. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine(); // Discard the leftover newline left behind by nextInt()
if (choice == 1) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter monthly salary: ");
double monthlySalary = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new SalariedEmployee(nextId, name, monthlySalary));
nextId++;
System.out.println("Salaried employee added.");
} else if (choice == 2) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter hours worked: ");
double hours = scanner.nextDouble();
System.out.print("Enter hourly rate: ");
double rate = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new HourlyEmployee(nextId, name, hours, rate));
nextId++;
System.out.println("Hourly employee added.");
} else if (choice == 3) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter base salary: ");
double base = scanner.nextDouble();
System.out.print("Enter sales amount: ");
double sales = scanner.nextDouble();
System.out.print("Enter commission rate (e.g. 0.05 for 5%): ");
double rate = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new CommissionEmployee(nextId, name, base, sales, rate));
nextId++;
System.out.println("Commission employee added.");
} else if (choice == 4) {
payroll.runPayroll();
} else if (choice == 5) {
System.out.print("Enter ID to find: ");
int id = scanner.nextInt();
scanner.nextLine();
Employee found = payroll.findById(id);
if (found == null) {
System.out.println("No employee with that ID.");
} else {
found.printPayslip();
}
} else if (choice == 6) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again."); // Catches anything outside 1-6
}
} while (choice != 6); // Keep looping until the user explicitly picks Exit
scanner.close();
}
}

Complete Code

Here is the full program with every type assembled in the correct order, ready to save as `EmployeePayrollSystem.java` and run with `javac EmployeePayrollSystem.java && java EmployeePayrollSystem`.

import java.util.Scanner;
interface Payable {
double calculateSalary();
}
abstract class Employee implements Payable {
protected int id;
protected String name;
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public void printPayslip() {
System.out.printf("ID: %-4d | Name: %-18s | Type: %-12s | Salary: %.2f%n",
id, name, getEmployeeType(), calculateSalary());
}
public abstract String getEmployeeType();
}
class SalariedEmployee extends Employee {
private double monthlySalary;
public SalariedEmployee(int id, String name, double monthlySalary) {
super(id, name);
this.monthlySalary = monthlySalary;
}
@Override
public double calculateSalary() {
return monthlySalary;
}
@Override
public String getEmployeeType() {
return "Salaried";
}
}
class HourlyEmployee extends Employee {
private double hoursWorked;
private double hourlyRate;
public HourlyEmployee(int id, String name, double hoursWorked, double hourlyRate) {
super(id, name);
this.hoursWorked = hoursWorked;
this.hourlyRate = hourlyRate;
}
@Override
public double calculateSalary() {
double regularHours = Math.min(hoursWorked, 160);
double overtimeHours = Math.max(0, hoursWorked - 160);
return (regularHours * hourlyRate) + (overtimeHours * hourlyRate * 1.5);
}
@Override
public String getEmployeeType() {
return "Hourly";
}
}
class CommissionEmployee extends Employee {
private double baseSalary;
private double salesAmount;
private double commissionRate;
public CommissionEmployee(int id, String name, double baseSalary, double salesAmount, double commissionRate) {
super(id, name);
this.baseSalary = baseSalary;
this.salesAmount = salesAmount;
this.commissionRate = commissionRate;
}
@Override
public double calculateSalary() {
return baseSalary + (salesAmount * commissionRate);
}
@Override
public String getEmployeeType() {
return "Commission";
}
}
class Payroll {
private Employee[] employees;
private int count = 0;
public Payroll(int capacity) {
employees = new Employee[capacity];
}
public boolean addEmployee(Employee e) {
if (count >= employees.length) {
System.out.println("Payroll is full; cannot add more employees.");
return false;
}
employees[count] = e;
count++;
return true;
}
public void runPayroll() {
System.out.println("\n--- Payroll Report ---");
double total = 0;
for (int i = 0; i < count; i++) {
employees[i].printPayslip();
total += employees[i].calculateSalary();
}
System.out.printf("Total payroll this period: %.2f%n", total);
}
public Employee findById(int id) {
for (int i = 0; i < count; i++) {
if (employees[i].getId() == id) {
return employees[i];
}
}
return null;
}
}
public class EmployeePayrollSystem {
public static void main(String[] args) {
Payroll payroll = new Payroll(50);
Scanner scanner = new Scanner(System.in);
int nextId = 1;
int choice;
do {
System.out.println("\n===== EMPLOYEE PAYROLL SYSTEM =====");
System.out.println("1. Add Salaried Employee");
System.out.println("2. Add Hourly Employee");
System.out.println("3. Add Commission Employee");
System.out.println("4. Run Payroll Report");
System.out.println("5. Find Employee by ID");
System.out.println("6. Exit");
System.out.print("Enter your choice: ");
choice = scanner.nextInt();
scanner.nextLine();
if (choice == 1) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter monthly salary: ");
double monthlySalary = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new SalariedEmployee(nextId, name, monthlySalary));
nextId++;
System.out.println("Salaried employee added.");
} else if (choice == 2) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter hours worked: ");
double hours = scanner.nextDouble();
System.out.print("Enter hourly rate: ");
double rate = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new HourlyEmployee(nextId, name, hours, rate));
nextId++;
System.out.println("Hourly employee added.");
} else if (choice == 3) {
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter base salary: ");
double base = scanner.nextDouble();
System.out.print("Enter sales amount: ");
double sales = scanner.nextDouble();
System.out.print("Enter commission rate (e.g. 0.05 for 5%): ");
double rate = scanner.nextDouble();
scanner.nextLine();
payroll.addEmployee(new CommissionEmployee(nextId, name, base, sales, rate));
nextId++;
System.out.println("Commission employee added.");
} else if (choice == 4) {
payroll.runPayroll();
} else if (choice == 5) {
System.out.print("Enter ID to find: ");
int id = scanner.nextInt();
scanner.nextLine();
Employee found = payroll.findById(id);
if (found == null) {
System.out.println("No employee with that ID.");
} else {
found.printPayslip();
}
} else if (choice == 6) {
System.out.println("Goodbye!");
} else {
System.out.println("Invalid choice, try again.");
}
} while (choice != 6);
scanner.close();
}
}

Sample Run

Sample Run

Click Run to see what this code prints.

Extend This Project

  • Add a second interface, `Bonusable`, with a `double calculateBonus()` method, and implement it only on the employee types that should receive a year-end bonus.
  • Replace the fixed-size `Employee[]` with a `List<Employee>` and compare the two approaches directly — note how `addEmployee()` no longer needs a capacity check.
  • Add a `TaxCalculator` utility class with a static method that applies a progressive tax rate to `calculateSalary()`'s result before printing.
  • Sort the payroll report by salary using `Arrays.sort(employees, 0, count, Comparator.comparingDouble(Employee::calculateSalary))`.
  • Persist each payroll run to a CSV file with `java.io.FileWriter`, one line per employee, so historical payroll reports can be reviewed later.

Summary

You built a payroll system where three genuinely different salary formulas coexist behind one shared `Payable` promise and one `abstract Employee` class, stored together in a single fixed-size `Employee[]` array and processed by one polymorphic loop in `runPayroll()`. The interface-plus-abstract-class combination you used here — a contract with no implementation at all, and a partial implementation that still leaves the core method abstract — is a pattern you will see constantly in larger Java codebases, anywhere a family of types needs to share some behavior while still guaranteeing every member can answer one specific question in its own way.