DEV Community

Cover image for 2 - Clean Architecture: Entities and Business Logic
Daniel Azevedo
Daniel Azevedo

Posted on

2 - Clean Architecture: Entities and Business Logic

Welcome back! In the first post, we introduced the basics of Clean Architecture and laid the foundation by creating our first entity: Employee. Today, we’ll dig deeper into Entities, explaining their role in Clean Architecture, and explore how to keep your business rules clean and maintainable, adhering to best practices. We'll also extend our payroll system example by adding some additional rules and behaviors to the Employee entity.

The Role of Entities in Clean Architecture

In Clean Architecture, Entities are the core business objects. They represent the fundamental data and logic that govern the domain you're working in. These should be independent of any application-specific logic or external systems, such as databases or the user interface.

An Entity:

  • Should encapsulate core business rules and data.
  • Must be independent of frameworks, UI, databases, or other external systems.
  • Should have no knowledge of how it is used within the application.

For example, in a payroll system, an Employee entity would store basic information like the employee's name, salary, and tax rate and could include simple behaviors directly tied to the employee’s attributes (e.g., validating the tax rate). However, complex business logic—like calculating the employee’s total salary based on benefits, deductions, or specific use cases—would not live in the entity. This is where Use Cases come into play.

Best Practices for Entities

To ensure your entities adhere to best practices, consider the following guidelines:

  1. Keep It Simple: Entities should focus solely on attributes and basic behaviors. Avoid adding complex business logic or validation that can clutter the entity and make it hard to maintain.

  2. Single Responsibility Principle: Each entity should have one reason to change. This means it should only contain the logic directly related to the data it encapsulates.

  3. Encapsulation: Protect your entity's state by exposing properties through public methods or properties, rather than allowing direct access. This helps maintain integrity and ensures that any invariants are enforced.

  4. Avoid Side Effects: Methods within an entity should not cause side effects outside of the entity itself. This keeps your entities predictable and easier to test.

  5. Use Value Objects for Simple Data: If you have complex types (like monetary values or dates), consider creating Value Objects to represent these. This can help enforce rules and maintain consistency.

Keep Your Entities Simple

Let's recap how our Employee entity looks:



public class Employee
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public decimal Salary { get; private set; }
    public decimal TaxRate { get; private set; }
    public decimal Benefits { get; private set; }

    public Employee(int id, string name, decimal salary, decimal taxRate, decimal benefits)
    {
        Id = id;
        Name = name;
        Salary = salary;
        TaxRate = taxRate;
        Benefits = benefits;
    }

    public void ValidateTaxRate()
    {
        if (TaxRate < 0 || TaxRate > 1)
        {
            throw new ArgumentException("Invalid tax rate");
        }
    }

    public decimal CalculateTotalBeforeTax()
    {
        return Salary + Benefits;
    }
}


Enter fullscreen mode Exit fullscreen mode

Adding More Rules to Our Entity

Let's say we want to introduce the concept of benefits that influence the total salary. We could expand our Employee entity to include benefits as an attribute and provide a method to calculate the total salary before taxes.

Here’s how we might modify the Employee entity to include benefits:



public class Employee
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public decimal Salary { get; private set; }
    public decimal TaxRate { get; private set; }
    public decimal Benefits { get; private set; }

    public Employee(int id, string name, decimal salary, decimal taxRate, decimal benefits)
    {
        Id = id;
        Name = name;
        Salary = salary;
        TaxRate = taxRate;
        Benefits = benefits;
    }

    public void ValidateTaxRate()
    {
        if (TaxRate < 0 || TaxRate > 1)
        {
            throw new ArgumentException("Invalid tax rate");
        }
    }

    public decimal CalculateTotalBeforeTax()
    {
        return Salary + Benefits;
    }
}


Enter fullscreen mode Exit fullscreen mode

What Changed?

  1. Private Setters: We changed the setters for the properties to private, enforcing encapsulation and ensuring the entity's state can only be modified through methods.

  2. Validation: The ValidateTaxRate method checks the tax rate’s validity.

  3. CalculateTotalBeforeTax Method: This method provides a simple calculation related to the employee's salary, keeping the logic directly tied to its data.

Business Logic in Use Cases, Not in Entities

Now, let’s discuss where to place more complex logic. Suppose we want to calculate the net salary (after taxes and deductions). This calculation might involve multiple rules and should be placed in the Use Case layer, not in the entity.

Let’s create a new Use Case that handles this logic. Here’s an example of how we might implement a payroll use case that calculates the employee's net salary:



public class ProcessPayrollUseCase
{
    public decimal CalculateNetSalary(Employee employee)
    {
        employee.ValidateTaxRate();
        decimal totalBeforeTax = employee.CalculateTotalBeforeTax();
        return totalBeforeTax - (totalBeforeTax * employee.TaxRate);
    }
}


Enter fullscreen mode Exit fullscreen mode

In this use case, we:

  1. Validate the employee’s tax rate using a method provided by the Employee entity.
  2. Calculate the total salary before tax by summing the salary and benefits.
  3. Apply the tax to get the final net salary.

By putting this logic in the use case, we ensure that the Employee entity remains clean and focused on its core responsibility: representing an employee’s data. Meanwhile, the ProcessPayrollUseCase encapsulates the more complex logic that involves calculations and workflows specific to payroll processing.

Keep Entities Reusable and Independent

One of the biggest advantages of keeping your entities simple is that they become reusable across multiple use cases. For example, the same Employee entity can be reused in different workflows, such as:

  • Salary calculation
  • Tax reporting
  • Performance evaluation
  • Employee benefits administration

Because the entity itself doesn’t contain any use case-specific logic, it can be adapted to different contexts without modification.

Conclusion

In this post, we dove deeper into the concept of Entities in Clean Architecture while adhering to best practices. We expanded our Employee entity to include benefits, kept the entity’s behavior simple, and demonstrated how to offload complex business logic into a Use Case.

Next up, we’ll focus more on Use Cases—how they drive the application’s behavior and orchestrate the flow between entities and external systems. We'll also cover how to design and implement them effectively.

Stay tuned for part 3!

Top comments (0)