DEV Community

Cover image for Clean Architecture and Either Monad
Marco Sabatini for Ticino Software Craft

Posted on • Updated on

Clean Architecture and Either Monad

References

Github Repo

Introduction

This post explores the development of a simple application that sends birthday greetings to employees. The primary goals of this exercise were to:

  1. Practice functional programming concepts, particularly the use of monads.
  2. Explore clean architecture principles by separating domain logic from infrastructure code.
  3. Enhance testability by decoupling components and injecting dependencies.

Design Approach

The application follows a functional programming style, modeling the core logic as a composition of pure functions. The Either monad from the Arrow library was chosen to explicitly handle success and error cases in the function signatures, making the code more readable and explicit.

// Success case
Either.Right(ResponseType())

// Error case
Either.Left(ErrorType("This is the Error!"))
Enter fullscreen mode Exit fullscreen mode

The application's responsibilities were divided into three main areas:

  1. Loading Employees: Retrieving employee data from a data source (a file in this exercise).
  2. Domain Logic: Filtering employees whose birthday is today.
  3. Sending Notifications: Dispatching birthday greetings to the filtered employees.

These responsibilities were encapsulated in separate functions, facilitating testability and modularity:

loadEmployees: () -> Either<MyError, Employees>
filterEmployees: (Employees) -> BirthdayEmployees
sendBirthdayNotificationTo: (BirthdayEmployees) -> Either<MyError, Unit>
Enter fullscreen mode Exit fullscreen mode

The core use case was then composed by combining these functions:

fun sendGreetingsWith(
    loadEmployees: () -> Either<MyError, Employees>,
    filterEmployees: (Employees) -> BirthdayEmployees,
    sendBirthdayNotificationTo: (BirthdayEmployees) -> Either<MyError, Unit>
): () -> Either<MyError, Unit> = {
    loadEmployees()
        .map(filterEmployees)
        .flatMap(sendBirthdayNotificationTo)
}
Enter fullscreen mode Exit fullscreen mode

This design facilitated testability by allowing the injection of different implementations for each responsibility, enabling isolated testing of domain logic and infrastructure components.

Testing

The separation of concerns and the use of pure functions greatly simplified testing. For example, in the acceptance test, the loadEmployee logic could be replaced with an in-memory implementation, eliminating the need for additional infrastructure configuration:

@Test
internal fun oneEmployeeIsBornTodayAnotherIsNotBornToday() {
    val inMemoryLoadEmployee: () -> Either<Nothing, Employees> = { ... }

    val sendGreetings: () -> Either<MyError, Unit> =
        sendGreetingsWith(
            inMemoryLoadEmployee,
            todayBirthdayEmployees,
            sendMailWith("localhost", 9999)
        )

    val result = sendGreetings()

    // Assertions...
}
Enter fullscreen mode Exit fullscreen mode

Infrastructure components, such as file I/O or database access, were tested separately using integration tests:

@Test
internal fun loadEmployeeFromFile() {
    val loadEmployeeFromFile = loadEmployeeFrom("./target/test-classes/employees.txt")
    assertThat(loadEmployeeFromFile()).isEqualTo(Right(Employees(...)))
}

@Test
internal fun employeeNotValid() {
    val loadEmployeeFromFile = loadEmployeeFrom("./target/test-classes/employeesNotValid.txt")
    assertThat(loadEmployeeFromFile()).isEqualTo(Left(MyError.LoadEmployeesError("Error...")))
}
Enter fullscreen mode Exit fullscreen mode

This separation of tests ensured that domain logic and infrastructure concerns were thoroughly validated without coupling them in the same test suite.

Conclusion

The Birthday Greeting Kata provided an excellent opportunity to explore clean architecture principles and functional programming concepts in a practical setting. By separating domain logic from infrastructure code and leveraging the power of pure functions and monads, the resulting codebase exhibited improved readability, testability, and modularity.

The exercise demonstrated the benefits of embracing functional programming paradigms and architectural patterns that promote separation of concerns. These practices not only enhance code quality and maintainability but also facilitate the integration of new requirements or the replacement of infrastructure components with minimal impact on the core domain logic.

Top comments (0)