Haskell Legacy: Going event-first
Previously, we have reversed writes, which means that we write events, then we write the legacy tables.
As a reminder, we have some code looking like that:
interceptTrainEffectEvents =
  interpret $
    \case
      TrainCreate
        departureDate'@(DepartureDate departureDate)
        departureStation'@(DepartureStation departureStation)
        arrivalStation'@(ArrivalStation arrivalStation) -> do
          newTrainId <- embed $ TrainId' <$> randomRIO (1000000, 9999999)
          eventStored <-
            storeEvent
              (StreamId newTrainId.unTrainId')
              (EventNumber 0)
              TrainCreated
                { departureDate = departureDate,
                  departureStation = departureStation,
                  arrivalStation = arrivalStation
                }
          case eventStored of
            Left e -> throw $ EventStoreIAE e
            Right x -> do
              trainProjectionCreate newTrainId departureDate' departureStation' arrivalStation'
              return newTrainId
      TrainFetch trainId ->
        trainProjectionFetch trainId
This was a first step, however it's temporal coupling, one of the worst type of coupling.
It means that we have established correlation (things happen together), not causation (things happen because they are linked by the relationships).
I'll rehash just to make the point: if tests are green, it's merely coincidental.
Your code will break (or force to change) if you:
- Add/change a view (model)/query/projection
- Reorder events persistence
- Add event persistence elsewhere
Instead of calling legacy effects as backup, we can create an interceptor in charge of it:
apiEventProjection (StreamId streamId) =
  \case
    event@(TrainCreated {}) ->
      trainProjectionCreate
        trainId
        (DepartureDate event.departureDate)
        (DepartureStation event.departureStation)
        (ArrivalStation event.arrivalStation)
    event@(BookingCreated {}) ->
      bookingProjectionCreate trainId (BookingId' $ fromIntegral event.id) event.travelerName
    event@(BookingWithdrawn {}) ->
      bookingProjectionDelete (BookingId' $ fromIntegral event.id)
  where
    trainId = TrainId' streamId
For each event, we will perform an action to "synchronize" a stateful value.
It aims to be used with this interceptor:
interceptEventStoreWith f =
  intercept $
    \case
      StoreEvent streamId eventNumber event -> do
        result <- storeEvent streamId eventNumber event
        forM_ result $ \() ->
          raise $ f streamId event
        return result
      FetchEvents streamId ->
        fetchEvents streamId
It works as follows:
- Fallbacks on the real EventStoreeffect to persist the event
- If the persistence succeeded, run the callback to perform the projection
- Return the result
Note: it is a really simple implementation, in the previous real world implementations I have done, I have relied on diffs (i.e. pushing multiples events and making diffs on local state, so it can be persisted independently).
We can drop the calls from our interpreter:
interceptTrainEffectEvents =
  interpret $
    \case
      TrainCreate
        (DepartureDate departureDate)
        (DepartureStation departureStation)
        (ArrivalStation arrivalStation) -> do
          newTrainId <- embed $ TrainId' <$> randomRIO (1000000, 9999999)
          eventStored <-
            storeEvent
              (StreamId newTrainId.unTrainId')
              (EventNumber 0)
              TrainCreated
                { departureDate = departureDate,
                  departureStation = departureStation,
                  arrivalStation = arrivalStation
                }
          case eventStored of
            Left e -> throw $ EventStoreIAE e
            Right x -> return newTrainId
      TrainFetch trainId ->
        trainProjectionFetch trainId
and add it to our interpreters stacks:
runPersistent pool =
  liftIO
    . join
    . fmap (either (throwIO . iaeToYesod) return)
    . runM
    . runError
    . runEmbedded (flip runSqlPool pool)
    . interpretBookingEffectPersistent
    . interpretTrainProjectionEffectPersistent
    . interpretEventStorePersistent
    . interceptEventStoreWith apiEventProjection
    . interpretBookingEffectEvents
    . interceptTrainEffectEvents
Doing so allows us to add/change our views without impacting our main, event-sourced interpreters.