SkillAgentSearch skills...

PolysemyCleanArchitecture

Showcasing how the Polysemy library can be used to implement a REST application conforming to the guidelines of the Clean Architecture model.

Install / Use

/learn @thma/PolysemyCleanArchitecture

README

Implementing Clean Architecture with Haskell and Polysemy

Actions Status

tl;dr

This article shows how algebraic effect systems can be used to maintain a clear separation of concerns between different parts of software systems. From a practical programming perspective this improves composability and testability of software components.

I'm demonstrating this idea by using the Polysemy library to implement a multi-layered REST application conforming to the guidelines of the Clean Architecture model.

Motivation

While writing Why Haskell Matters I prepared a little demo application that was meant to showcase a cleanly designed REST application in Haskell. In particular, I wanted to demonstrate how the clear separation of pure and impure code helps to provide strict separation of concerns and state-of-the-art testability of all application layers.

I failed!

I was able to write the domain logic in pure code consisting only of total functions. It was a great pleasure to write unit tests for them!

However, as soon as I started writing controllers that coordinate access to the domain logic as well as to a persistence layer to retrieve and store data, I was stuck in the IO Monad. That is, in test cases I was not able to test the controllers independently of the concrete backend.

Then I tried to apply the final tagless pattern for the persistence layer. This allowed abstracting out the concrete persistence layer and writing controller tests with a mocked persistence backend. But when it came to testing the REST API handlers (written with Servant) I was again stuck in the IO Monad as the Handler type is defined as newtype Handler a = Handler { runHandler' :: ExceptT ServerError IO a }. Maybe it's not a principle issue but just my brain being too small...

I was desperately looking for something that allowed me to combine different types of effects (like persistence, logging, configuration, http handlers, error handling, etc.) in controllers and handlers but still to be able to write tests that allow using mocks or stubs to test components in isolation.

As I reached a dead end, I had a look at some of the algebraic effect systems available in Haskell, like eff, extensible-effects, fused-effects, freer-simple and Polysemy.

In algebraic effect systems, effectful programs are split into two separate parts: the specification of the effects to be performed, and the interpretation (or semantics) given to them.

So my idea was to provide special effect interpretations that would allow building mocked effects for my test suite.

After seeing a presentation on maintainable software architecture with Polysemy which answered many of my questions I rewrote my application based on Polysemy powered algebraic effects.

I'm pretty satisfied with the result, and of course I'm eager to share my approach with you!

The Challenge

A very small boutique restaurant (serving excellent vietnamese food) is looking for a reservation system that allows managing reservations. The restaurant has only twenty seats, they also take only a maximum of twenty reservations per day. (So guests can stay the whole evening and don't have to leave after some time.) (I adopted this scenario from a inspiring talk by Mark Seemann)

They have asked us to write the REST backend for their reservation system.

The chef insists on a scrupulously clean kitchen and is also a lover of clean code. He has read about clean architecture and wants his new software to be a perfect example!

So we cannot just hack away but first have to understand what is expected from us when we are to deliver a clean architecture.

What makes a Clean Architecture ?

I'm following the introduction to clean architecture by Robert C. Martin on his Clean Code blog. He states that his concept builds up on several earlier approaches like hexagonal architecture, ports and adapters or Onion Architecture.

According to him all these approaches share a similar objective: achieve separation of concerns by dividing a software system into different layers. All approaches result in system designs that share a common set of features:

  1. The architecture does not depend on any specific software libraries or frameworks. This allows to freely choose such tools according to the actual needs. This avoids "vendor lock in".

  2. High testability. The business logic can be tested without any external element like UI, DB, Web Server, etc.

  3. The UI is loosely coupled to the core system. So it can be easily changed or replaced without affecting the rest of the system.

  4. The Database is also "external" to the core system. It can be easily changed (even from an RDBMS to NonSQL DB) without affecting the business logic.

  5. The Business logic is agnostic of the outside world. It has no dependencies to any external systems like DB, ESB, etc.

Layers with clearly separated responsibilities

The architecture consists of four layers, each of which contains components with a specific scope and a limited set of responsibilities.

  1. At the centre sits the Domain layer consisting of entities and core business logic.

  2. Next comes the Use Cases layer where all resources are coordinated that are required to fulfill a given use case. In particular, it uses entities and logic from the domain layer to implement use cases. But typically it must also interface to a persistent storage to retrieve and store entities.

  3. The Interface Adapters layer holds code for UI controllers and presenters as well as adapters to external resources like databases, message queues, configuration, Logging, etc.

  4. The External Interfaces layer contains the technical implementation of external interfaces. For example, a concrete REST service assembly, Web and UI infrastructure, databases, etc.

The Dependency Rule

The overriding rule that makes this architecture work is The Dependency Rule. This rule says that source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. In particular, the name of something declared in an outer circle must not be mentioned by the code in the an inner circle. That includes, functions, classes. variables, or any other named software entity.

Quoted from Clean Architecture blog post

This dependency rule leads to a very interesting consequence: If a use case interactor needs to access a component from an outer circle, e.g. retrieve data from a database, this must be done in a specific way in order to avoid breaking the dependency rule: In the use case layer we don't have any knowledge about the components of the outer circles. If we require access to a database (or any other external resources), the call interface, as well as the data transfer protocol must be specified in the use case layer.

The components in the outer circles will then implement this interface. Using this kind of interfaces, it is possible to communicate accross the layer boundaries, but still maintain a strict separation of concerns.

If you want to dive deeper into clean architecture I recommend the Clean Architecture blog post as an entry point. Robert C. Martin later also published a whole book Clean Architecture: A Craftsman's Guide to Software Structure and Design on this concept.

In the following sections I'll explain how the clean architecture guidelines can be implemented in a Haskell REST API application by making use of the algebraic effect library Polysemy.

The Domain layer

The ReservationDomain module implements the business logic for seat reservations in a very small boutique restaurant. The restaurant has only one big table with 20 seats. Each day the restaurants accepts only 20 reservations. (There is no limited time-slot for each guest.)

Please note:

  • all functions in this module are pure (they don't do any IO) and total (they produce defined results for all possible input values).

  • The definitions in this module do not have dependencies to anything from the outer circles.

At the core of our Domain lies the Reservation data type:

-- | a data type representing a reservation
data Reservation = Reservation
    { date     :: Day    -- ^ the date of the reservation
    , name     :: String -- ^ the name of the guest placing the reservation
    , email    :: String -- ^ the email address of the guest
    , quantity :: Natural    -- ^ how many seats are requested
    }
    deriving (Eq, Generic, Read, Show)

This type can be used to express facts like Mr. Miller reserved two seats on 2020-06-01, he can be reached via his email address: manfred@miller.com:

reservation = Reservation {name = "Mr. Miller", quantity = 2, date = read "2020-06-01", email = "manfred@miller.com"}

All reservations of a specific day are represented as a list of reservations: [Reservation].

A ReservationMap is a map from Day to [Reservation]:

-- | a key value map holding a list of reservations for any given day
View on GitHub
GitHub Stars193
CategoryData
Updated2mo ago
Forks17

Languages

Haskell

Security Score

100/100

Audited on Dec 30, 2025

No findings