6. Backend-Agnostic Code
Whenever possible, write code that depends on interfaces rather than concrete implementations. This enables switching backends: use PostgreSQL in production for durability, but the blazing-fast Memory backend for tests. Here’s how to do it.
6.1. Prerequisites
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RequiredTypeArguments #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Main where
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM (atomically, newTVarIO, readTVarIO, modifyTVar')
import Control.Exception (bracket)
import Control.Monad (void)
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import Data.UUID.V4 qualified as UUID
import GHC.Generics (Generic)
import Hindsight
import Hindsight.Store.Memory (newMemoryStore)
import Hindsight.Store.Filesystem (newFilesystemStore, mkDefaultConfig, cleanupFilesystemStore)
import System.Directory (removeDirectoryRecursive)
import System.IO.Temp (createTempDirectory, getCanonicalTemporaryDirectory)
6.2. Define Events
type TaskCreated = "task_created"
data TaskInfo = TaskInfo
{ taskId :: Text
, taskName :: Text
} deriving (Show, Eq, Generic, FromJSON, ToJSON)
-- Event versioning
type instance MaxVersion TaskCreated = 0
type instance Versions TaskCreated = '[TaskInfo]
instance Event TaskCreated
instance MigrateVersion 0 TaskCreated
-- Helper
createTask :: Text -> Text -> SomeLatestEvent
createTask tid name =
mkEvent TaskCreated (TaskInfo tid name)
6.3. Write Backend-Agnostic Functions
Use the EventStore constraint to write functions that work with any
backend:
-- Works with Memory, Filesystem, PostgreSQL - any EventStore backend
processTask :: forall backend. (EventStore backend, StoreConstraints backend IO)
=> BackendHandle backend
-> Text -- Task ID
-> Text -- Task name
-> IO Int -- Returns count of all tasks
processTask store taskId taskName = do
streamId <- StreamId <$> UUID.nextRandom
-- Insert the task event
void $ insertEvents store Nothing $
singleEvent streamId Any (createTask taskId taskName)
-- Count all tasks by subscribing to events
countVar <- newTVarIO (0 :: Int)
handle <- subscribe store
(match TaskCreated (countHandler countVar) :? MatchEnd)
(EventSelector AllStreams FromBeginning)
threadDelay 100000 -- Wait for subscription
handle.cancel
readTVarIO countVar
where
countHandler countVar _envelope = do
atomically $ modifyTVar' countVar (+1)
return Continue
6.4. Use with Different Backends
The same function works with any backend:
demoWithMemory :: IO ()
demoWithMemory = do
putStrLn "=== Using Memory Backend ==="
store <- newMemoryStore
count1 <- processTask store "T1" "Learn Haskell"
putStrLn $ " After task 1: " <> show count1 <> " tasks"
count2 <- processTask store "T2" "Write docs"
putStrLn $ " After task 2: " <> show count2 <> " tasks\n"
demoWithFilesystem :: IO ()
demoWithFilesystem = do
putStrLn "=== Using Filesystem Backend ==="
tmpDir <- getCanonicalTemporaryDirectory
storePath <- createTempDirectory tmpDir "hindsight-tutorial"
bracket
(newFilesystemStore $ mkDefaultConfig storePath)
cleanupFilesystemStore
$ \store -> do
count1 <- processTask store "T3" "Fix bug"
putStrLn $ " After task 1: " <> show count1 <> " tasks"
count2 <- processTask store "T4" "Deploy"
putStrLn $ " After task 2: " <> show count2 <> " tasks"
removeDirectoryRecursive storePath
putStrLn " (Cleaned up)\n"
6.5. Running the Examples
main :: IO ()
main = do
putStrLn "=== Hindsight Tutorial 06: Backend-Agnostic Code ==="
putStrLn ""
demoWithMemory
demoWithFilesystem
putStrLn "Tutorial complete!"
6.6. Summary
Key concepts:
EventStore constraint: Makes functions work with any backend
Same logic, different storage: Write once, test with Memory, deploy with PostgreSQL
Type inference: Haskell figures out the backend type from usage
6.7. Next Steps
You now have all the core concepts! In the final tutorial, we’ll explore advanced features and deployment patterns.