Type-safe event sourcing for Haskell with strong compile-time guarantees.
Hindsight is an opinionated event sourcing library for Haskell. A defining feature of Hindsight is to
make event versioning a compile-time concern, by separating the definition of an event (identifed by a typelevel Symbol)
from that of its successive payloads. By default, migrations are handled automatically through upcasting of successive versions
(but you can opt-out of that mechanism when you see fit).
The testing toolkit generates roundtrip and golden tests automatically to ensure you never accidentally break compatibility with stored events.
Moreover, Hindsight defines an event store interface featuring:
These primitives allow you to implement arbitrary optimistic-concurrency control mechanisms, as well as common event sourcing patterns (sagas, process managers, etc.)
Three store implementations are provided:
Finally, a PostgreSQL-based (but store-agnostic) projection system is provided. As a store-specific feature, the PostgreSQL store supports synchronous projections (called inline projections in Marten DB). Synchronous projections are particularly useful to:
Define an event with type-level versioning:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hindsight
import Hindsight.Store.Memory (newMemoryStore)
-- Event name at the type level
type UserRegistered = "user_registered"
-- Event payload
data UserInfo = UserInfo
{ userId :: Text
, userName :: Text
} deriving (Show, Eq, Generic, FromJSON, ToJSON)
-- Version declaration (version 0)
type instance MaxVersion UserRegistered = 0
type instance Versions UserRegistered = '[UserInfo]
instance Event UserRegistered
instance MigrateVersion 0 UserRegistered
Store and retrieve events:
import Data.UUID.V4 qualified as UUID
example :: IO ()
example = do
store <- newMemoryStore
streamId <- StreamId <$> UUID.nextRandom
let event = mkEvent "user_registered" (UserInfo "U001" "Alice")
result <- insertEvents store Nothing $
singleEvent streamId Any [event]
handle <- subscribe store
(match "user_registered" handleEvent :? MatchEnd)
(EventSelector AllStreams FromBeginning)
...
where
handleEvent envelope = do
print envelope.payload.userName
return Continue
Event Stores: Stream-based event storage with exactly-once delivery and total ordering
Projections: SQL-based read models
Type-Safe Versioning: Compile-time event schema evolution with automatic migration composition
Consistency: Optimistic locking, multi-stream transactions, synchronous projections
BSD-3-Clause. See LICENSE for details.
42 commits
Haskell
97.7%
Type-safe event sourcing for Haskell with strong compile-time guarantees.
Hindsight is an opinionated event sourcing library for Haskell. A defining feature of Hindsight is to
make event versioning a compile-time concern, by separating the definition of an event (identifed by a typelevel Symbol)
from that of its successive payloads. By default, migrations are handled automatically through upcasting of successive versions
(but you can opt-out of that mechanism when you see fit).
The testing toolkit generates roundtrip and golden tests automatically to ensure you never accidentally break compatibility with stored events.
Moreover, Hindsight defines an event store interface featuring:
These primitives allow you to implement arbitrary optimistic-concurrency control mechanisms, as well as common event sourcing patterns (sagas, process managers, etc.)
Three store implementations are provided:
Finally, a PostgreSQL-based (but store-agnostic) projection system is provided. As a store-specific feature, the PostgreSQL store supports synchronous projections (called inline projections in Marten DB). Synchronous projections are particularly useful to:
Define an event with type-level versioning:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TypeFamilies #-}
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hindsight
import Hindsight.Store.Memory (newMemoryStore)
-- Event name at the type level
type UserRegistered = "user_registered"
-- Event payload
data UserInfo = UserInfo
{ userId :: Text
, userName :: Text
} deriving (Show, Eq, Generic, FromJSON, ToJSON)
-- Version declaration (version 0)
type instance MaxVersion UserRegistered = 0
type instance Versions UserRegistered = '[UserInfo]
instance Event UserRegistered
instance MigrateVersion 0 UserRegistered
Store and retrieve events:
import Data.UUID.V4 qualified as UUID
example :: IO ()
example = do
store <- newMemoryStore
streamId <- StreamId <$> UUID.nextRandom
let event = mkEvent "user_registered" (UserInfo "U001" "Alice")
result <- insertEvents store Nothing $
singleEvent streamId Any [event]
handle <- subscribe store
(match "user_registered" handleEvent :? MatchEnd)
(EventSelector AllStreams FromBeginning)
...
where
handleEvent envelope = do
print envelope.payload.userName
return Continue
Event Stores: Stream-based event storage with exactly-once delivery and total ordering
Projections: SQL-based read models
Type-Safe Versioning: Compile-time event schema evolution with automatic migration composition
Consistency: Optimistic locking, multi-stream transactions, synchronous projections
BSD-3-Clause. See LICENSE for details.
42 commits
Haskell
97.7%