├── .gitignore ├── Makefile ├── README.md ├── frequent_flier_account.go ├── frequent_flier_events.go ├── main.go └── status_string.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-event-sourcing-sample 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @go generate && go build 3 | 4 | clean: 5 | @rm -f goes 6 | @rm -f *_string.go 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ###Event Sourcing in Go 2 | 3 | This is the repository for the code which accompanies my post on Event Sourcing in Go. The original post is [here](http://jen20.com/2015/02/08/event-sourcing-in-go.html). 4 | 5 | To build and run: 6 | 7 | - `make` 8 | - `./goes` 9 | 10 | To clean generated code and the binary: 11 | 12 | - `make clean` 13 | -------------------------------------------------------------------------------- /frequent_flier_account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // FrequentFlierAccount represents the state of an instance of the frequent flier 6 | // account aggregate. It tracks changes on itself in the form of domain events. 7 | type FrequentFlierAccount struct { 8 | id string 9 | miles int 10 | tierPoints int 11 | status Status 12 | expectedVersion int 13 | changes []interface{} 14 | } 15 | 16 | // RecordFlightTaken is used to record the fact that a customer has taken a flight 17 | // which should be attached to this frequent flier account. The number of miles and 18 | // tier points which apply are calculated externally. 19 | // 20 | // If recording this flight takes the account over a status boundary, it will 21 | // automatically upgrade the account to the new status level. 22 | func (self *FrequentFlierAccount) RecordFlightTaken(miles int, tierPoints int) { 23 | self.trackChange(FlightTaken{MilesAdded: miles, TierPointsAdded: tierPoints}) 24 | 25 | if self.tierPoints > 20 && self.status != StatusGold { 26 | self.trackChange(PromotedToGoldStatus{}) 27 | } 28 | } 29 | 30 | // String implements Stringer for FrequentFlierAccount instances. 31 | func (a FrequentFlierAccount) String() string { 32 | format := `FrequentFlierAccount: %s 33 | Miles: %d 34 | TierPoints: %d 35 | Status: %s 36 | (Expected Version: %d) 37 | (Pending Changes: %d) 38 | ` 39 | return fmt.Sprintf(format, a.id, a.miles, a.tierPoints, a.status, a.expectedVersion, len(a.changes)) 40 | } 41 | 42 | // trackChange is used internally by bevhavious methods to apply a state change to 43 | // the current instance and also track it in order that it can be persisted later. 44 | func (state *FrequentFlierAccount) trackChange(event interface{}) { 45 | state.changes = append(state.changes, event) 46 | state.transition(event) 47 | } 48 | 49 | // transition imnplements the pattern match against event types used both as part 50 | // of the fold when loading from history and when tracking an individual change. 51 | func (state *FrequentFlierAccount) transition(event interface{}) { 52 | switch e := event.(type) { 53 | 54 | case FrequentFlierAccountCreated: 55 | state.id = e.AccountId 56 | state.miles = e.OpeningMiles 57 | state.tierPoints = e.OpeningTierPoints 58 | state.status = StatusRed 59 | 60 | case StatusMatched: 61 | state.status = e.NewStatus 62 | 63 | case FlightTaken: 64 | state.miles = state.miles + e.MilesAdded 65 | state.tierPoints = state.tierPoints + e.TierPointsAdded 66 | 67 | case PromotedToGoldStatus: 68 | state.status = StatusGold 69 | } 70 | } 71 | 72 | // NewFrequentFlierAccountFromHistory creates a FrequentFlierAccount given a history 73 | // of the changes which have occurred for that account. 74 | func NewFrequentFlierAccountFromHistory(events []interface{}) *FrequentFlierAccount { 75 | state := &FrequentFlierAccount{} 76 | for _, event := range events { 77 | state.transition(event) 78 | state.expectedVersion++ 79 | } 80 | return state 81 | } 82 | -------------------------------------------------------------------------------- /frequent_flier_events.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Status represents the Red, Silver or Gold tier level of a FrequentFlierAccount 4 | type Status int 5 | 6 | const ( 7 | StatusRed Status = iota 8 | StatusSilver Status = iota 9 | StatusGold Status = iota 10 | ) 11 | 12 | // go:generate stringer -type=Status 13 | 14 | type FrequentFlierAccountCreated struct { 15 | AccountId string 16 | OpeningMiles int 17 | OpeningTierPoints int 18 | } 19 | 20 | type StatusMatched struct { 21 | NewStatus Status 22 | } 23 | 24 | type FlightTaken struct { 25 | MilesAdded int 26 | TierPointsAdded int 27 | } 28 | 29 | type PromotedToGoldStatus struct{} 30 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | history := []interface{}{ 9 | FrequentFlierAccountCreated{AccountId: "1234567", OpeningMiles: 10000, OpeningTierPoints: 0}, 10 | StatusMatched{NewStatus: StatusSilver}, 11 | FlightTaken{MilesAdded: 2525, TierPointsAdded: 5}, 12 | FlightTaken{MilesAdded: 2512, TierPointsAdded: 5}, 13 | FlightTaken{MilesAdded: 5600, TierPointsAdded: 5}, 14 | FlightTaken{MilesAdded: 3000, TierPointsAdded: 3}, 15 | } 16 | 17 | aggregate := NewFrequentFlierAccountFromHistory(history) 18 | fmt.Println("Before RecordFlightTaken") 19 | fmt.Println(aggregate) 20 | 21 | aggregate.RecordFlightTaken(1000, 3) 22 | fmt.Println("After RecordFlightTaken") 23 | fmt.Println(aggregate) 24 | } 25 | -------------------------------------------------------------------------------- /status_string.go: -------------------------------------------------------------------------------- 1 | // generated by stringer -type=Status; DO NOT EDIT 2 | 3 | package main 4 | 5 | import "fmt" 6 | 7 | const _Status_name = "StatusRedStatusSilverStatusGold" 8 | 9 | var _Status_index = [...]uint8{0, 9, 21, 31} 10 | 11 | func (i Status) String() string { 12 | if i < 0 || i >= Status(len(_Status_index)-1) { 13 | return fmt.Sprintf("Status(%d)", i) 14 | } 15 | return _Status_name[_Status_index[i]:_Status_index[i+1]] 16 | } 17 | --------------------------------------------------------------------------------