Last active
December 16, 2022 00:09
-
-
Save akhansari/095414e79ad3b3e6a20f4047c651e08f to your computer and use it in GitHub Desktop.
F# : Event Sourcing in a nutshell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ========= 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) | |
| = | |
| 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