MyNixOS website logo
Description

Batteries included event sourcing and CQRS.

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 GETs and POSTs.
  • 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. Querys 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.

Metadata

Version

0.5.0

Platforms (77)

    Darwin
    FreeBSD
    Genode
    GHCJS
    Linux
    MMIXware
    NetBSD
    none
    OpenBSD
    Redox
    Solaris
    WASI
    Windows
Show all
  • aarch64-darwin
  • aarch64-freebsd
  • aarch64-genode
  • aarch64-linux
  • aarch64-netbsd
  • aarch64-none
  • aarch64-windows
  • aarch64_be-none
  • arm-none
  • armv5tel-linux
  • armv6l-linux
  • armv6l-netbsd
  • armv6l-none
  • armv7a-darwin
  • armv7a-linux
  • armv7a-netbsd
  • armv7l-linux
  • armv7l-netbsd
  • avr-none
  • i686-cygwin
  • i686-darwin
  • i686-freebsd
  • i686-genode
  • i686-linux
  • i686-netbsd
  • i686-none
  • i686-openbsd
  • i686-windows
  • javascript-ghcjs
  • loongarch64-linux
  • m68k-linux
  • m68k-netbsd
  • m68k-none
  • microblaze-linux
  • microblaze-none
  • microblazeel-linux
  • microblazeel-none
  • mips-linux
  • mips-none
  • mips64-linux
  • mips64-none
  • mips64el-linux
  • mipsel-linux
  • mipsel-netbsd
  • mmix-mmixware
  • msp430-none
  • or1k-none
  • powerpc-netbsd
  • powerpc-none
  • powerpc64-linux
  • powerpc64le-linux
  • powerpcle-none
  • riscv32-linux
  • riscv32-netbsd
  • riscv32-none
  • riscv64-linux
  • riscv64-netbsd
  • riscv64-none
  • rx-none
  • s390-linux
  • s390-none
  • s390x-linux
  • s390x-none
  • vc4-none
  • wasm32-wasi
  • wasm64-wasi
  • x86_64-cygwin
  • x86_64-darwin
  • x86_64-freebsd
  • x86_64-genode
  • x86_64-linux
  • x86_64-netbsd
  • x86_64-none
  • x86_64-openbsd
  • x86_64-redox
  • x86_64-solaris
  • x86_64-windows