// ========= 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 [] 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 = "jdoe@veepee.com" } |> handleCommand userId printStep () printfn "\n==== Step 3: Activate Account" User.Activate |> handleCommand userId printStep () // to run the script: > dotnet fsi UserAggregate.fsx