DEV Community

Cover image for Layered Architecture in C# – MVC in a Payroll System
Daniel Azevedo
Daniel Azevedo

Posted on

Layered Architecture in C# – MVC in a Payroll System

Layered Architecture on a payroll processing system in C#. Even though this approach might seem overkill for smaller applications, it’s been incredibly useful for keeping everything organized and easy to maintain, especially as the project grows.

For those unfamiliar, the idea behind Layered Architecture is to split your application into different layers, where each layer has its own responsibility. This keeps the code cleaner, easier to test, and makes changes less risky.

So, What Is Layered Architecture?

In a nutshell, it’s a way of structuring your application into separate layers:

  1. Presentation Layer – Handles the UI or API, basically where user input and output happen.
  2. Business Logic Layer – Where all your core business logic sits, like the actual rules for calculating payroll.
  3. Data Access Layer (DAL) – Takes care of storing and retrieving data from databases or other storage systems.
  4. Infrastructure Layer – This can be for things like logging or third-party integrations (optional, but helpful).

By separating concerns into these layers, you make your codebase much easier to manage over time. Now, let me break it down with a concrete example.

Example: Payroll Processing in C

Imagine we need to process salaries for employees, applying taxes and generating payslips. Here’s how I used Layered Architecture to make that happen.

1. Presentation Layer (Controller)

This layer should simply collect the user input (like employee details, hours worked) and pass it to the business layer. It doesn’t deal with any business logic. In a simple API or console application, it would look something like this:

public class PayrollController
{
    private readonly PayrollService _payrollService;

    public PayrollController(PayrollService payrollService)
    {
        _payrollService = payrollService;
    }

    public void ProcessPayroll(int employeeId)
    {
        // Fetch employee data from the service
        var result = _payrollService.ProcessEmployeePayroll(employeeId);

        // Output the processed salary
        Console.WriteLine($"Processed salary for {result.Employee.Name}: {result.Salary}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the Controller is only concerned with calling the service and outputting the result — no business logic here. It delegates all of that responsibility to the PayrollService.

2. Business Logic Layer

This is where all the core rules for payroll processing live. For example, it will calculate the gross salary based on hours worked, apply tax deductions, and return the final salary. The PayrollService interacts with the EmployeeRepository to retrieve employee data and apply business rules.

public class PayrollService
{
    private readonly ITaxService _taxService;
    private readonly EmployeeRepository _employeeRepository;

    public PayrollService(ITaxService taxService, EmployeeRepository employeeRepository)
    {
        _taxService = taxService;
        _employeeRepository = employeeRepository;
    }

    public PayrollResult ProcessEmployeePayroll(int employeeId)
    {
        // Fetch employee data
        var employee = _employeeRepository.GetEmployeeById(employeeId);

        // Calculate base salary based on hours worked
        var grossSalary = employee.BaseSalary * (employee.HoursWorked / 160m);

        // Apply tax deduction
        var tax = _taxService.CalculateTax(grossSalary);
        var netSalary = grossSalary - tax;

        return new PayrollResult
        {
            Employee = employee,
            Salary = netSalary
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The PayrollService is responsible for all payroll-related business logic, keeping it decoupled from how the data is fetched or how the results are presented.

3. Data Access Layer (DAL)

The Data Access Layer is responsible for interacting with the database or external storage systems. It fetches employee data from the database and provides it to the business logic layer.

public class EmployeeRepository
{
    public Employee GetEmployeeById(int id)
    {
        // Simulating fetching from a database
        return new Employee { Id = id, Name = "Jane Doe", BaseSalary = 5000, HoursWorked = 160 };
    }
}
Enter fullscreen mode Exit fullscreen mode

This keeps the data-related logic in its own layer, allowing you to change the storage mechanism (database, API, etc.) without affecting the business logic.

4. Infrastructure Layer (Optional)

This layer handles things like logging, sending emails, or integrating with external services. For example, after processing payroll, we might want to send an email with the payslip.

public class EmailService
{
    public void SendPaySlip(Employee employee, decimal salary)
    {
        // Simulating sending an email
        Console.WriteLine($"Sending payslip to {employee.Name}, Salary: {salary}");
    }
}
Enter fullscreen mode Exit fullscreen mode

One of the main reasons I like using Layered Architecture is that it makes everything much more modular. For instance, if I need to update how taxes are calculated, I only need to touch the business layer—without worrying about breaking the database access or UI code.

It also makes the application easier to test. Since the business logic is isolated in its own layer, you can easily mock dependencies like the EmployeeRepository or TaxService and focus on testing the logic in isolation.

While Layered Architecture might seem like overkill for smaller apps, as projects grow in complexity, it really pays off. It makes the codebase easier to understand, maintain, and extend. And when you're dealing with something as critical as payroll processing, having a well-structured architecture makes all the difference.

Keep coding!!

Top comments (0)