Skip to content

Instantly share code, notes, and snippets.

@akhansari
Last active December 16, 2022 00:09
Show Gist options
  • Select an option

  • Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.

Select an option

Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.
F# : Event Sourcing in a nutshell
// ========= event sourcing in a nutshell
(*
AggregateName: string
Aggregate friendly name.
Initial: 'State
Initial (empty) state we will start with.
Decide: 'Command -> 'State -> 'Event list
Given the current state and what has been requested, decide what should happen.
Evolve: 'State -> 'Event -> 'State
Given the current state and what happened, evolve to a new state.
Build: 'State -> 'Event list -> 'State
Given the current state and the history, build the state.
Rebuild: 'Event list -> 'State
Rebuild the current state from the entire history.
*)
// ========= domain layer
[<RequireQualifiedAccess>]
module User =
let AggregateName = "User"
type Info =
{ Name: string
Age: int
Email: string }
// state is internal to the domain
// depending on requirements it could be anything
// as it's not stored, it could be fixed and replayed, same as projections
type State =
{ Name: string option
Age: int option
Email: string option
IsActive: bool }
let initialState =
{ Name = None
Age = None
Email = None
IsActive = false }
type Event =
| Registered of Info
| Activated
| EmailModified of string
type Command =
| Register of Info
| Activate
| ModifyEmail of string
let private evolve (state: State) (event: Event) : State =
match event with
| Registered userInfo ->
{ state with
Name = Some userInfo.Name
Age = Some userInfo.Age
Email = Some userInfo.Email
IsActive = false }
| Activated ->
{ state with IsActive = true }
| EmailModified email ->
{ state with Email = Some email }
let decide (command: Command) (state: State) : Event list =
match command, state with
| Register userInfo, _ -> [ Registered userInfo ]
| Activate, { IsActive = false } -> [ Activated ]
| Activate, { IsActive = true } -> [ ]
| ModifyEmail email, _ -> [ EmailModified email ]
let build = List.fold evolve
let rebuild = build initialState
// ========= application layer
module AppWithoutEventStore =
// event sourcing could be used only on the domain layer without an event store
// then only the state is loaded and saved but not really recommended
let RegisterUser
(save: User.State -> unit)
(userInfo: User.Info)
=
let currentState = User.initialState
User.decide (User.Register userInfo) currentState
|> User.build User.initialState
|> save
let ModifyEmail
(load: unit -> User.State)
(save: User.State -> unit)
email
=
let currentState = load ()
User.decide (User.ModifyEmail email) currentState
|> User.build currentState
|> save
type AggregateKey =
{ Name: string
Id: string }
module App =
// with an event store, everything becomes a Royce
let handleCommand
(read: AggregateKey -> User.Event list) // read history dep
(write: AggregateKey -> User.Event list -> unit) // write new events dep
(userId: string)
(command: User.Command)
=
let aggregateKey =
{ Name = User.AggregateName
Id = userId }
read aggregateKey
|> User.rebuild
|> User.decide command
|> write aggregateKey
// ========= infra layer
module EventStore =
// event store can be anything, depending on the context
open System.Collections.Generic
let db = Dictionary ()
let read key =
match db.TryGetValue key with
| true, events -> events
| _ -> []
let write key events =
let history = read key
db.[key] <- history @ events
// ========= startup layer
let handleCommand =
App.handleCommand
EventStore.read
EventStore.write
let userId = "abc123"
let aggregateKey = { Name = User.AggregateName; Id = userId }
let printStep () =
EventStore.read aggregateKey
|> printfn "database:\n%A"
EventStore.read aggregateKey
|> User.rebuild
|> printfn "state:\n%A"
printfn "==== Step 1 : Empty EventStore"
printStep ()
printfn "\n==== Step 2 : Register User"
User.Register
{ Name = "John Doe"; Age = 42; Email = "[email protected]" }
|> handleCommand userId
printStep ()
printfn "\n==== Step 3: Activate Account"
User.Activate
|> handleCommand userId
printStep ()
// to run the script: > dotnet fsi UserAggregate.fsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment