DomainDriven
DomainDriven is a batteries included synchronous event sourcing and CQRS library. The goal of this library is to allow you to implement DDD principles without focusing on the boilerplate.
It uses Template Haskell
we generate a Servant server from the specification and we aim to keep the specification as succinct as we can.
The idea
- Use a GADT to specify the actions, what will be translated into
GET
s and POST
s.
- Make each event update run in a transaction, thereby avoiding the eventual consistency issues commonly associated with event sourcing.
How it works
In order to implement a model in domaindriven
you have to define:
- The model (current state)
- The events
- How to update the model when new events come in
- The actions (queries and commands)
- How to handle actions
Model
The model is the current state of the system. This is what you normally would keep in a database, but as this is an event sourced system the state is not fundamental as it can be recalculated.
Currently all implemented persistence strategies all keep the state in memory.
Events
Events are things that happened in the past. The event you define represent all the changes that can occur in the system.
Events should be specified in past tens.
data Event
= IncreasedCounter
| DecreasedCounter
Event handler
The model is calculated as a fold over the stream of events. As events happened in the past we can never refuse to handle them. This means the event handler is simply:
applyEvent :: Model -> Stored Event -> Model
where Stored is defined as:
data Stored a = Stored
{ storedEvent :: a
, storedTimestamp :: UTCTime
, storedUUID :: UUID
}
Commands
Commands are defined using a GADT with one type parameter representing the return type. For example:
-- Same as: data StorageAction (x :: ParamPart) method a where
data StorageAction :: Action where
GetFile
:: P x "fileId" UUID
-> StorageAction x Query ByteString
AddFile
:: P x "fileContent" ByteString
-> StorageAction Cmd UUID
RemoveFile
:: P x "fileId" UUID
-> StorageAction Cmd ()
Action handler
Actions, in contrast to events, are allowed to fail. If an action succeeds we need to return a value of the type specified by the constructor and, if it was a command, a list of events. The action handler do not update the state.
In addition you may need to make requests, read from disk, or perform other side effects in order to calculate the result.
ActionHandler
is defined as:
type ActionHandler model event m c =
forall method a. c 'ParamType method a -> HandlerType method model event m a
In practice this means you specify actions as
data CounterAction x method return where
GetCounter ::CounterAction x Query Int
IncreaseCounter ::CounterAction x Cmd Int
DecreaseCounter ::CounterAction x Cmd Int
and the corresponding handler as
handleAction :: ActionHandler CounterAction CounterEvent IO a
handleAction = \case
GetCounter -> Query $ pure -- Query is just `model -> IO a`
IncreaseCounter -> Cmd $ \_ -> `model -> IO (model -> a, [CounterEvent])`
pure (id -- return state as is, after the event is applied
, [CounterIncreased])
DecreaseCounter -> Cmd $ \counter -> do
when (counter < 1) (throwM NegativeNotSupported)
pure (id, [CounterDecreased])
A Query
takes a model -> m a
, i.e. you get access to the model and the ability to run monadic efficts. Query
s will be translates into GET
in the generated API.
A Cmd
has the additional ability of emitting events. It takes a model -> m (model -> a, [event])
. The return value is specified as a function from the updated model to the return type. This way we can, in the Counter example, return the new value after the event handler has run.
Generating the server
Now we have defined the core parts of our service. We can now generate the server using the template-haskell function mkServer
. It takes two arguments: The server config and the name of the GADT representing the actions. E.g. $(mkServer counterActionConfig ''CounterAction)
.
The ServerConfig
, storeActionConfig
in this example, contains the API options for the for the Action and all it's sub actions, as well as a all parameter names. This can be tenerated with $(mkServerConfig "counterActionConfig")
, but due to TemplateHaskell's stage restrictions it cannot run in the same file as mkServer
.
Simple example
Minimal example can be found in examples/simple/Main.hs, this uses the model defined in models/Models/Counter.hs