├── .gitignore ├── .gitmodules ├── README.md ├── src └── mcveat │ └── cqrs │ ├── account │ ├── command.go │ ├── model.go │ ├── model_test.go │ ├── service.go │ └── service_test.go │ ├── cqrs │ └── main.go │ ├── event │ └── event.go │ ├── listener │ └── listener.go │ ├── store │ ├── event_store.go │ ├── event_store_test.go │ └── model.go │ └── transfer │ ├── command.go │ ├── model.go │ ├── model_test.go │ ├── service.go │ └── service_test.go └── vendor └── src └── github.com ├── go-check └── .gitkeep └── nu7hatch └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | pkg/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/src/github.com/go-check/check"] 2 | path = vendor/src/github.com/go-check/check 3 | url = git@github.com:go-check/check.git 4 | branch = v1 5 | [submodule "vendor/src/github.com/nu7hatch/gouuid"] 6 | path = vendor/src/github.com/nu7hatch/gouuid 7 | url = git@github.com:nu7hatch/gouuid.git 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | 4 | Example of Event Sourcing and CQRS in golang. Based on: 5 | 6 | * https://github.com/pinballjs/event-sourcing-cqrs 7 | * https://github.com/cer/event-sourcing-examples 8 | 9 | Instructions 10 | ============ 11 | 12 | Required: 13 | 14 | * go ver. 1.5.x 15 | * gb https://github.com/constabulary/gb 16 | 17 | ``` 18 | git clone git@github.com:mcveat/event-sourcing-cqrs.git 19 | cd event-sourcing-cqrs 20 | git submodule init 21 | git submodule update 22 | gb build all 23 | ./bin/cqrs 24 | ``` 25 | 26 | To test: 27 | 28 | ``` 29 | gb test 30 | ``` 31 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/account/command.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import . "github.com/nu7hatch/gouuid" 4 | 5 | type Command interface{} 6 | 7 | type OpenAccount struct { 8 | InitialBalance int 9 | } 10 | 11 | type Credit struct { 12 | Uuid *UUID 13 | Amount int 14 | } 15 | 16 | type Debit struct { 17 | Uuid *UUID 18 | Amount int 19 | } 20 | 21 | type CreditOnTransfer struct { 22 | Transaction *UUID 23 | Amount int 24 | From *UUID 25 | To *UUID 26 | } 27 | 28 | type DebitOnTransfer struct { 29 | Transaction *UUID 30 | Amount int 31 | From *UUID 32 | To *UUID 33 | } 34 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/account/model.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | . "mcveat/cqrs/store" 8 | ) 9 | 10 | type Account struct { 11 | Uuid *UUID 12 | Balance int 13 | } 14 | 15 | func (a Account) String() string { 16 | return fmt.Sprint("{Account: uuid=", a.Uuid, " balance=", a.Balance, "}") 17 | } 18 | 19 | func Build(history History) Account { 20 | account := Account{} 21 | for _, event := range history.Events { 22 | account = apply(event, account) 23 | } 24 | return account 25 | } 26 | 27 | func apply(e Event, account Account) Account { 28 | switch event := e.(type) { 29 | case AccountOpened: 30 | account.Uuid = event.Uuid 31 | account.Balance = event.InitialBalance 32 | case AccountCredited: 33 | account.Balance += event.Amount 34 | case AccountCreditedOnTransfer: 35 | account.Balance += event.Amount 36 | case AccountDebited: 37 | account.Balance -= event.Amount 38 | case AccountDebitedOnTransfer: 39 | account.Balance -= event.Amount 40 | } 41 | return account 42 | } 43 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/account/model_test.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | . "github.com/go-check/check" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | . "mcveat/cqrs/store" 8 | ) 9 | 10 | type MyModelSuite struct{} 11 | 12 | var _ = Suite(&MyModelSuite{}) 13 | 14 | func (s *MyModelSuite) TestBuildAccountWithNoEvents(c *C) { 15 | history := History{Events: []Event{}} 16 | account := Build(history) 17 | c.Assert(account.Uuid, IsNil) 18 | c.Assert(account.Balance, Equals, 0) 19 | } 20 | 21 | func (s *MyModelSuite) TestBuildOpenedAccount(c *C) { 22 | uuid, _ := NewV4() 23 | events := []Event{AccountOpened{uuid, 100}} 24 | history := History{Events: events} 25 | account := Build(history) 26 | c.Assert(account.Uuid, Equals, uuid) 27 | c.Assert(account.Balance, Equals, 100) 28 | } 29 | 30 | func (s *MyModelSuite) TestBuildCreditedAccount(c *C) { 31 | uuid, _ := NewV4() 32 | events := []Event{AccountOpened{uuid, 100}, AccountCredited{uuid, 50}} 33 | history := History{Events: events} 34 | account := Build(history) 35 | c.Assert(account.Uuid, Equals, uuid) 36 | c.Assert(account.Balance, Equals, 150) 37 | } 38 | 39 | func (s *MyModelSuite) TestBuildDebitedAccount(c *C) { 40 | uuid, _ := NewV4() 41 | events := []Event{AccountOpened{uuid, 100}, AccountDebited{uuid, 40}} 42 | history := History{Events: events} 43 | account := Build(history) 44 | c.Assert(account.Uuid, Equals, uuid) 45 | c.Assert(account.Balance, Equals, 60) 46 | } 47 | 48 | func (s *MyModelSuite) TestBuildAccountCreditedOnTransfer(c *C) { 49 | uuid, _ := NewV4() 50 | randomOtherUUID, _ := NewV4() 51 | events := []Event{AccountOpened{uuid, 100}, AccountCreditedOnTransfer{uuid, randomOtherUUID, 50, randomOtherUUID, uuid}} 52 | history := History{Events: events} 53 | account := Build(history) 54 | c.Assert(account.Uuid, Equals, uuid) 55 | c.Assert(account.Balance, Equals, 150) 56 | } 57 | 58 | func (s *MyModelSuite) TestBuildAccountDebitedOnTransfer(c *C) { 59 | uuid, _ := NewV4() 60 | randomOtherUUID, _ := NewV4() 61 | events := []Event{AccountOpened{uuid, 100}, AccountDebitedOnTransfer{uuid, randomOtherUUID, 20, randomOtherUUID, uuid}} 62 | history := History{Events: events} 63 | account := Build(history) 64 | c.Assert(account.Uuid, Equals, uuid) 65 | c.Assert(account.Balance, Equals, 80) 66 | } 67 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/account/service.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | "mcveat/cqrs/listener" 8 | . "mcveat/cqrs/store" 9 | ) 10 | 11 | type Service struct { 12 | store *EventStore 13 | } 14 | 15 | func NewService(es *EventStore) Service { 16 | return Service{es} 17 | } 18 | 19 | func (s *Service) Act(cmd Command) chan *UUID { 20 | done := make(chan *UUID) 21 | go s.act(cmd, done) 22 | return done 23 | } 24 | 25 | func (s *Service) act(c Command, done chan *UUID) { 26 | switch v := c.(type) { 27 | case OpenAccount: 28 | event := AccountOpened{InitialBalance: v.InitialBalance} 29 | for result := range s.store.Save([]Event{event}) { 30 | done <- result 31 | } 32 | case Credit: 33 | event := AccountCredited{v.Uuid, v.Amount} 34 | done <- s.actionOnAccount(v.Uuid, event) 35 | case Debit: 36 | event := AccountDebited{v.Uuid, v.Amount} 37 | done <- s.actionOnAccount(v.Uuid, event) 38 | case CreditOnTransfer: 39 | event := AccountCreditedOnTransfer{v.To, v.Transaction, v.Amount, v.From, v.To} 40 | done <- s.actionOnAccount(v.To, event) 41 | case DebitOnTransfer: 42 | event := AccountDebitedOnTransfer{v.From, v.Transaction, v.Amount, v.From, v.To} 43 | done <- s.actionOnAccount(v.From, event) 44 | } 45 | close(done) 46 | } 47 | 48 | func (s *Service) actionOnAccount(uuid *UUID, event Event) *UUID { 49 | account := <-s.store.Find(uuid) 50 | update := Update{uuid, []Event{event}, account.Version} 51 | err := <-s.store.Update(update) 52 | if err != nil { 53 | fmt.Println("Update on account failed", err) 54 | } 55 | return uuid 56 | } 57 | 58 | func (s *Service) StartListener() { 59 | go listener.Listen(s.store, s.handleEvent) 60 | } 61 | 62 | func (s *Service) handleEvent(e Event) chan *UUID { 63 | switch event := e.(type) { 64 | case TransferCreated: 65 | return s.Act(DebitOnTransfer{event.Uuid, event.Amount, event.From, event.To}) 66 | case AccountDebitedOnTransfer: 67 | return s.Act(CreditOnTransfer{event.Transaction, event.Amount, event.From, event.To}) 68 | } 69 | return nil 70 | } 71 | 72 | func (s *Service) Find(uuid *UUID) chan Account { 73 | done := make(chan Account) 74 | go func() { 75 | done <- Build(<-s.store.Find(uuid)) 76 | close(done) 77 | }() 78 | return done 79 | } 80 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/account/service_test.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | . "github.com/go-check/check" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | "mcveat/cqrs/store" 8 | "testing" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | TestingT(t) 13 | } 14 | 15 | type MySuite struct{} 16 | 17 | var _ = Suite(&MySuite{}) 18 | 19 | func (s *MySuite) TestOpenAccount(c *C) { 20 | es := store.Empty() 21 | as := Service{&es} 22 | uuid := <-as.Act(OpenAccount{InitialBalance: 100}) 23 | page := <-es.Events(0, 10) 24 | c.Assert(page.Events, HasLen, 1) 25 | history := <-es.Find(uuid) 26 | c.Assert(history.Events, HasLen, 1) 27 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: uuid, InitialBalance: 100}) 28 | c.Assert(history.Version, Equals, 1) 29 | } 30 | 31 | func (s *MySuite) TestCreditAccount(c *C) { 32 | es := store.Empty() 33 | as := Service{&es} 34 | uuid := <-as.Act(OpenAccount{InitialBalance: 100}) 35 | <-as.Act(Credit{uuid, 200}) 36 | page := <-es.Events(0, 10) 37 | c.Assert(page.Events, HasLen, 2) 38 | history := <-es.Find(uuid) 39 | c.Assert(history.Events, HasLen, 2) 40 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: uuid, InitialBalance: 100}) 41 | c.Assert(history.Events[1], Equals, AccountCredited{Uuid: uuid, Amount: 200}) 42 | c.Assert(history.Version, Equals, 2) 43 | } 44 | 45 | func (s *MySuite) TestDebitAccount(c *C) { 46 | es := store.Empty() 47 | as := Service{&es} 48 | uuid := <-as.Act(OpenAccount{InitialBalance: 100}) 49 | <-as.Act(Debit{uuid, 50}) 50 | page := <-es.Events(0, 10) 51 | c.Assert(page.Events, HasLen, 2) 52 | history := <-es.Find(uuid) 53 | c.Assert(history.Events, HasLen, 2) 54 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: uuid, InitialBalance: 100}) 55 | c.Assert(history.Events[1], Equals, AccountDebited{Uuid: uuid, Amount: 50}) 56 | c.Assert(history.Version, Equals, 2) 57 | } 58 | 59 | func (s *MySuite) TestCreditAccountOnTransfer(c *C) { 60 | es := store.Empty() 61 | as := Service{&es} 62 | uuid := <-as.Act(OpenAccount{InitialBalance: 100}) 63 | transaction, _ := NewV4() 64 | otherAccount, _ := NewV4() 65 | <-as.Act(CreditOnTransfer{transaction, 200, otherAccount, uuid}) 66 | page := <-es.Events(0, 10) 67 | c.Assert(page.Events, HasLen, 2) 68 | history := <-es.Find(uuid) 69 | c.Assert(history.Events, HasLen, 2) 70 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: uuid, InitialBalance: 100}) 71 | c.Assert(history.Events[1], Equals, AccountCreditedOnTransfer{uuid, transaction, 200, otherAccount, uuid}) 72 | c.Assert(history.Version, Equals, 2) 73 | } 74 | 75 | func (s *MySuite) TestDebitAccountOnTransfer(c *C) { 76 | es := store.Empty() 77 | as := Service{&es} 78 | uuid := <-as.Act(OpenAccount{InitialBalance: 100}) 79 | transaction, _ := NewV4() 80 | otherAccount, _ := NewV4() 81 | <-as.Act(DebitOnTransfer{transaction, 50, uuid, otherAccount}) 82 | page := <-es.Events(0, 10) 83 | c.Assert(page.Events, HasLen, 2) 84 | history := <-es.Find(uuid) 85 | c.Assert(history.Events, HasLen, 2) 86 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: uuid, InitialBalance: 100}) 87 | c.Assert(history.Events[1], Equals, AccountDebitedOnTransfer{uuid, transaction, 50, uuid, otherAccount}) 88 | c.Assert(history.Version, Equals, 2) 89 | } 90 | 91 | func (s *MySuite) TestHandleTransferCreatedEvent(c *C) { 92 | es := store.Empty() 93 | as := Service{&es} 94 | thisAccount := <-as.Act(OpenAccount{InitialBalance: 100}) 95 | otherAccount, _ := NewV4() 96 | transaction, _ := NewV4() 97 | 98 | <-as.handleEvent(TransferCreated{transaction, thisAccount, otherAccount, 50}) 99 | page := <-es.Events(0, 10) 100 | c.Assert(page.Events, HasLen, 2) 101 | 102 | history := <-es.Find(thisAccount) 103 | c.Assert(history.Events, HasLen, 2) 104 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: thisAccount, InitialBalance: 100}) 105 | c.Assert(history.Events[1], Equals, AccountDebitedOnTransfer{thisAccount, transaction, 50, thisAccount, otherAccount}) 106 | } 107 | 108 | func (s *MySuite) TestHandleAccountDebitedOnTransferEvent(c *C) { 109 | es := store.Empty() 110 | as := Service{&es} 111 | thisAccount := <-as.Act(OpenAccount{InitialBalance: 100}) 112 | otherAccount, _ := NewV4() 113 | transaction, _ := NewV4() 114 | 115 | <-as.handleEvent(AccountDebitedOnTransfer{otherAccount, transaction, 50, otherAccount, thisAccount}) 116 | page := <-es.Events(0, 10) 117 | c.Assert(page.Events, HasLen, 2) 118 | 119 | history := <-es.Find(thisAccount) 120 | c.Assert(history.Events, HasLen, 2) 121 | c.Assert(history.Events[0], Equals, AccountOpened{Uuid: thisAccount, InitialBalance: 100}) 122 | c.Assert(history.Events[1], Equals, AccountCreditedOnTransfer{thisAccount, transaction, 50, otherAccount, thisAccount}) 123 | } 124 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/cqrs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "mcveat/cqrs/account" 6 | "mcveat/cqrs/store" 7 | "mcveat/cqrs/transfer" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | es := store.Empty() 13 | as := account.NewService(&es) 14 | ts := transfer.NewService(&es) 15 | 16 | as.StartListener() 17 | ts.StartListener() 18 | 19 | firstAccount := <-as.Act(account.OpenAccount{InitialBalance: 100}) 20 | secondAccount := <-as.Act(account.OpenAccount{InitialBalance: 0}) 21 | 22 | as.Act(account.Credit{Uuid: firstAccount, Amount: 300}) 23 | transfer := ts.Act(transfer.CreateTransfer{firstAccount, secondAccount, 125}) 24 | 25 | time.Sleep(1000 * time.Millisecond) 26 | 27 | fmt.Println("Accounts:") 28 | fmt.Println("---------") 29 | fmt.Println(<-as.Find(firstAccount)) 30 | fmt.Println(<-as.Find(secondAccount)) 31 | fmt.Println("Transfers:") 32 | fmt.Println("----------") 33 | fmt.Println(<-ts.Find(<-transfer)) 34 | fmt.Println("Events:") 35 | fmt.Println("-------") 36 | page := <-es.Events(0, 100) 37 | for _, event := range page.Events { 38 | fmt.Println(event) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/event/event.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | ) 7 | 8 | type Event interface { 9 | SetUUID(uuid *UUID) Event 10 | String() string 11 | } 12 | 13 | type AccountOpened struct { 14 | Uuid *UUID 15 | InitialBalance int 16 | } 17 | 18 | func (e AccountOpened) String() string { 19 | return fmt.Sprint("{AccountOpened: uuid=", e.Uuid, " initialBalance=", e.InitialBalance, "}") 20 | } 21 | 22 | func (e AccountOpened) SetUUID(uuid *UUID) Event { 23 | e.Uuid = uuid 24 | return e 25 | } 26 | 27 | type AccountCredited struct { 28 | Uuid *UUID 29 | Amount int 30 | } 31 | 32 | func (e AccountCredited) String() string { 33 | return fmt.Sprint("{AccountCredited: uuid=", e.Uuid, " amount=", e.Amount, "}") 34 | } 35 | 36 | func (e AccountCredited) SetUUID(uuid *UUID) Event { 37 | e.Uuid = uuid 38 | return e 39 | } 40 | 41 | type AccountDebited struct { 42 | Uuid *UUID 43 | Amount int 44 | } 45 | 46 | func (e AccountDebited) String() string { 47 | return fmt.Sprint("{AccountDebited: uuid=", e.Uuid, " amount=", e.Amount, "}") 48 | } 49 | 50 | func (e AccountDebited) SetUUID(uuid *UUID) Event { 51 | e.Uuid = uuid 52 | return e 53 | } 54 | 55 | type AccountCreditedOnTransfer struct { 56 | Uuid *UUID 57 | Transaction *UUID 58 | Amount int 59 | From *UUID 60 | To *UUID 61 | } 62 | 63 | func (e AccountCreditedOnTransfer) String() string { 64 | return fmt.Sprint("{AccountCreditedOnTransfer: amount=", e.Amount, ", from=", e.From, "}") 65 | } 66 | 67 | func (e AccountCreditedOnTransfer) SetUUID(uuid *UUID) Event { 68 | e.Uuid = uuid 69 | return e 70 | } 71 | 72 | type AccountDebitedOnTransfer struct { 73 | Uuid *UUID 74 | Transaction *UUID 75 | Amount int 76 | From *UUID 77 | To *UUID 78 | } 79 | 80 | func (e AccountDebitedOnTransfer) String() string { 81 | return fmt.Sprint("{AccountDebitedOnTransfer: amount=", e.Amount, " to=", e.To, "}") 82 | } 83 | 84 | func (e AccountDebitedOnTransfer) SetUUID(uuid *UUID) Event { 85 | e.Uuid = uuid 86 | return e 87 | } 88 | 89 | type TransferCreated struct { 90 | Uuid *UUID 91 | From *UUID 92 | To *UUID 93 | Amount int 94 | } 95 | 96 | func (e TransferCreated) String() string { 97 | return fmt.Sprint("{TransferCreated: from=", e.From, " to=", e.To, " amount=", e.Amount, "}") 98 | } 99 | 100 | func (e TransferCreated) SetUUID(uuid *UUID) Event { 101 | e.Uuid = uuid 102 | return e 103 | } 104 | 105 | type TransferDebited struct { 106 | Uuid *UUID 107 | From *UUID 108 | To *UUID 109 | Amount int 110 | } 111 | 112 | func (e TransferDebited) String() string { 113 | return fmt.Sprint("{TransferDebited: from=", e.From, " to=", e.To, " amount=", e.Amount, "}") 114 | } 115 | 116 | func (e TransferDebited) SetUUID(uuid *UUID) Event { 117 | e.Uuid = uuid 118 | return e 119 | } 120 | 121 | type TransferCredited struct { 122 | Uuid *UUID 123 | From *UUID 124 | To *UUID 125 | Amount int 126 | } 127 | 128 | func (e TransferCredited) String() string { 129 | return fmt.Sprint("{TransferCredited: from=", e.From, " to=", e.To, " amount=", e.Amount, "}") 130 | } 131 | 132 | func (e TransferCredited) SetUUID(uuid *UUID) Event { 133 | e.Uuid = uuid 134 | return e 135 | } 136 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | . "github.com/nu7hatch/gouuid" 5 | . "mcveat/cqrs/event" 6 | . "mcveat/cqrs/store" 7 | "time" 8 | ) 9 | 10 | func Listen(es *EventStore, handle func(Event) chan *UUID) { 11 | offset := 0 12 | for { 13 | time.Sleep(100 * time.Millisecond) 14 | page := <-es.Events(offset, 5) 15 | for _, event := range page.Events { 16 | handle(event) 17 | } 18 | offset = page.Offset 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/store/event_store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | "sync" 8 | ) 9 | 10 | type EventStore struct { 11 | store map[UUID][]Event 12 | log []Event 13 | mux sync.Mutex 14 | } 15 | 16 | func Empty() EventStore { 17 | return EventStore{store: make(map[UUID][]Event), log: make([]Event, 0)} 18 | } 19 | 20 | func (es *EventStore) Save(events []Event) chan *UUID { 21 | done := make(chan *UUID) 22 | go es.save(events, done) 23 | return done 24 | } 25 | 26 | func (es *EventStore) save(events []Event, done chan *UUID) { 27 | uuid, err := NewV4() 28 | if err != nil { 29 | panic(err) 30 | } 31 | eventsWithParent := addUUID(uuid, events) 32 | es.synchronous(func() { 33 | es.store[(*uuid)] = eventsWithParent 34 | es.log = append(es.log, eventsWithParent...) 35 | }) 36 | done <- uuid 37 | close(done) 38 | } 39 | 40 | func (es *EventStore) Find(uuid *UUID) chan History { 41 | done := make(chan History) 42 | go es.find(uuid, done) 43 | return done 44 | } 45 | 46 | func (es *EventStore) find(uuid *UUID, done chan History) { 47 | events, ok := es.store[(*uuid)] 48 | if !ok { 49 | events = make([]Event, 0) 50 | } 51 | done <- History{events, len(events)} 52 | close(done) 53 | } 54 | 55 | func (es *EventStore) Update(update Update) chan error { 56 | done := make(chan error) 57 | go es.synchronous(func() { 58 | es.update(update, done) 59 | }) 60 | return done 61 | } 62 | 63 | func (es *EventStore) update(update Update, done chan error) { 64 | defer close(done) 65 | stored, ok := es.store[(*update.Uuid)] 66 | if !ok { 67 | done <- fmt.Errorf("Called update on entity that does not exists:", update) 68 | return 69 | } 70 | if len(stored) != update.Version { 71 | done <- fmt.Errorf("Optimistic lock failed on update:", update) 72 | return 73 | } 74 | eventsWithParent := addUUID(update.Uuid, update.Events) 75 | es.store[(*update.Uuid)] = append(stored, eventsWithParent...) 76 | es.log = append(es.log, eventsWithParent...) 77 | done <- nil 78 | } 79 | 80 | func (es *EventStore) synchronous(f func()) { 81 | es.mux.Lock() 82 | f() 83 | es.mux.Unlock() 84 | } 85 | 86 | func (es *EventStore) Events(offset int, batchsize int) chan Page { 87 | done := make(chan Page) 88 | go es.events(offset, batchsize, done) 89 | return done 90 | } 91 | 92 | func (es *EventStore) events(offset int, batchSize int, done chan Page) { 93 | defer close(done) 94 | noOfEvents := len(es.log) 95 | if noOfEvents == 0 { 96 | done <- Page{0, make([]Event, 0)} 97 | return 98 | } 99 | if offset < 0 { 100 | offset = 0 101 | } 102 | if offset >= noOfEvents { 103 | done <- Page{offset, make([]Event, 0)} 104 | return 105 | } 106 | if batchSize <= 0 { 107 | batchSize = 10 108 | } 109 | max := offset + batchSize 110 | if max > noOfEvents { 111 | max = noOfEvents 112 | } 113 | result := es.log[offset:max] 114 | done <- Page{max, result} 115 | } 116 | 117 | func addUUID(uuid *UUID, events []Event) []Event { 118 | result := make([]Event, 0, len(events)) 119 | for _, e := range events { 120 | result = append(result, e.SetUUID(uuid)) 121 | } 122 | return result 123 | } 124 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/store/event_store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/go-check/check" 6 | . "github.com/nu7hatch/gouuid" 7 | . "mcveat/cqrs/event" 8 | "testing" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | TestingT(t) 13 | } 14 | 15 | type MySuite struct{} 16 | 17 | var _ = Suite(&MySuite{}) 18 | 19 | type GenericEvent struct { 20 | uuid *UUID 21 | value int 22 | } 23 | 24 | func (e GenericEvent) SetUUID(uuid *UUID) Event { 25 | e.uuid = uuid 26 | return e 27 | } 28 | 29 | func (e GenericEvent) String() string { 30 | return fmt.Sprint("{GenericEvent: value=%g}", e.value) 31 | } 32 | 33 | func (s *MySuite) TestSaveEventsInStore(c *C) { 34 | es := Empty() 35 | uuid := <-es.Save([]Event{GenericEvent{value: 42}, GenericEvent{value: -1}}) 36 | c.Assert(uuid, NotNil) 37 | c.Assert(es.store, HasLen, 1) 38 | events := es.store[(*uuid)] 39 | c.Assert(events, HasLen, 2) 40 | c.Assert(events[0], Equals, GenericEvent{uuid: uuid, value: 42}) 41 | c.Assert(events[1], Equals, GenericEvent{uuid: uuid, value: -1}) 42 | } 43 | 44 | func (s *MySuite) TestSaveEventsInLog(c *C) { 45 | es := Empty() 46 | firstUuid := <-es.Save([]Event{GenericEvent{value: 1}, GenericEvent{value: 2}}) 47 | secondUuid := <-es.Save([]Event{GenericEvent{value: 3}, GenericEvent{value: 4}}) 48 | c.Assert(firstUuid, Not(Equals), secondUuid) 49 | log := es.log 50 | c.Assert(log, HasLen, 4) 51 | c.Assert(log[0], Equals, GenericEvent{uuid: firstUuid, value: 1}) 52 | c.Assert(log[1], Equals, GenericEvent{uuid: firstUuid, value: 2}) 53 | c.Assert(log[2], Equals, GenericEvent{uuid: secondUuid, value: 3}) 54 | c.Assert(log[3], Equals, GenericEvent{uuid: secondUuid, value: 4}) 55 | } 56 | 57 | func (s *MySuite) TestFindMissing(c *C) { 58 | es := Empty() 59 | randomUUID, _ := NewV4() 60 | history := <-es.Find(randomUUID) 61 | c.Assert(history.Events, HasLen, 0) 62 | c.Assert(history.Version, Equals, 0) 63 | } 64 | 65 | func (s *MySuite) TestFind(c *C) { 66 | es := Empty() 67 | uuid := <-es.Save([]Event{GenericEvent{value: 42}}) 68 | history := <-es.Find(uuid) 69 | c.Assert(history.Events, HasLen, 1) 70 | c.Assert(history.Version, Equals, 1) 71 | } 72 | 73 | func (s *MySuite) TestUpdateNotExisting(c *C) { 74 | es := Empty() 75 | randomUUID, _ := NewV4() 76 | update := Update{randomUUID, []Event{}, 1} 77 | err := <-es.Update(update) 78 | c.Assert(err, ErrorMatches, "Called update on entity that does not exists.*") 79 | } 80 | 81 | func (s *MySuite) TestUpdateOptimisticLockFailed(c *C) { 82 | es := Empty() 83 | uuid := <-es.Save([]Event{GenericEvent{value: 42}, GenericEvent{value: 43}}) 84 | update := Update{uuid, []Event{}, 1} 85 | err := <-es.Update(update) 86 | c.Assert(err, ErrorMatches, "Optimistic lock failed on update.*") 87 | } 88 | 89 | func (s *MySuite) TestUpdate(c *C) { 90 | es := Empty() 91 | uuid := <-es.Save([]Event{GenericEvent{value: 42}, GenericEvent{value: 43}}) 92 | update := Update{uuid, []Event{GenericEvent{value: 44}}, 2} 93 | err := <-es.Update(update) 94 | c.Assert(err, IsNil) 95 | c.Assert(es.store[(*uuid)], HasLen, 3) 96 | c.Assert(es.log, HasLen, 3) 97 | } 98 | 99 | func (s *MySuite) TestEventsOnEmptySet(c *C) { 100 | es := Empty() 101 | page := <-es.Events(0, 20) 102 | c.Assert(page.Offset, Equals, 0) 103 | c.Assert(page.Events, HasLen, 0) 104 | } 105 | 106 | func (s *MySuite) TestEventsBadOffset(c *C) { 107 | es := storeWithEvents(50) 108 | page := <-es.Events(-5, 20) 109 | c.Assert(page.Offset, Equals, 20) 110 | } 111 | 112 | func (s *MySuite) TestEventsBadBatchSize(c *C) { 113 | es := storeWithEvents(50) 114 | page := <-es.Events(0, -20) 115 | c.Assert(page.Offset, Equals, 10) 116 | } 117 | 118 | func (s *MySuite) TestEvents(c *C) { 119 | es := storeWithEvents(50) 120 | page := <-es.Events(10, 10) 121 | c.Assert(page.Offset, Equals, 20) 122 | c.Assert(page.Events, HasLen, 10) 123 | } 124 | 125 | func (s *MySuite) TestEventsOnBoundary(c *C) { 126 | es := storeWithEvents(15) 127 | page := <-es.Events(10, 10) 128 | c.Assert(page.Offset, Equals, 15) 129 | c.Assert(page.Events, HasLen, 5) 130 | } 131 | 132 | func storeWithEvents(n int) EventStore { 133 | es := Empty() 134 | for i := 0; i < n; i++ { 135 | <-es.Save([]Event{GenericEvent{value: i}}) 136 | } 137 | return es 138 | } 139 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/store/model.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | . "github.com/nu7hatch/gouuid" 5 | . "mcveat/cqrs/event" 6 | ) 7 | 8 | type History struct { 9 | Events []Event 10 | Version int 11 | } 12 | 13 | type Update struct { 14 | Uuid *UUID 15 | Events []Event 16 | Version int 17 | } 18 | 19 | type Page struct { 20 | Offset int 21 | Events []Event 22 | } 23 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/transfer/command.go: -------------------------------------------------------------------------------- 1 | package transfer 2 | 3 | import . "github.com/nu7hatch/gouuid" 4 | 5 | type Command interface{} 6 | 7 | type CreateTransfer struct { 8 | From *UUID 9 | To *UUID 10 | Amount int 11 | } 12 | 13 | type Debite struct { 14 | Uuid *UUID 15 | From *UUID 16 | To *UUID 17 | Amount int 18 | } 19 | 20 | type Complete struct { 21 | Uuid *UUID 22 | From *UUID 23 | To *UUID 24 | Amount int 25 | } 26 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/transfer/model.go: -------------------------------------------------------------------------------- 1 | package transfer 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | . "mcveat/cqrs/store" 8 | ) 9 | 10 | const ( 11 | Created = iota 12 | Debited = iota 13 | Completed = iota 14 | ) 15 | 16 | type Transfer struct { 17 | Uuid *UUID 18 | From *UUID 19 | To *UUID 20 | Amount int 21 | Status int 22 | debited bool 23 | credited bool 24 | } 25 | 26 | func (t Transfer) String() string { 27 | return fmt.Sprint("{Transfer: uuid=", t.Uuid, ", from=", t.From, " to=", t.To, " amount=", t.Amount, 28 | " status=", statusString(t.Status), "}") 29 | } 30 | 31 | func statusString(status int) string { 32 | switch status { 33 | case Created: 34 | return "created" 35 | case Debited: 36 | return "debited" 37 | case Completed: 38 | return "completed" 39 | } 40 | return "unknown" 41 | } 42 | 43 | func Build(history History) Transfer { 44 | transfer := Transfer{} 45 | for _, event := range history.Events { 46 | transfer = apply(event, transfer) 47 | } 48 | return transfer 49 | } 50 | 51 | func apply(e Event, transfer Transfer) Transfer { 52 | switch event := e.(type) { 53 | case TransferCreated: 54 | transfer.Uuid = event.Uuid 55 | transfer.From = event.From 56 | transfer.To = event.To 57 | transfer.Amount = event.Amount 58 | transfer.Status = Created 59 | case TransferDebited: 60 | transfer.debited = true 61 | case TransferCredited: 62 | transfer.credited = true 63 | } 64 | if transfer.debited && transfer.credited { 65 | transfer.Status = Completed 66 | } else if transfer.debited { 67 | transfer.Status = Debited 68 | } 69 | return transfer 70 | } 71 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/transfer/model_test.go: -------------------------------------------------------------------------------- 1 | package transfer 2 | 3 | import ( 4 | . "github.com/go-check/check" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | . "mcveat/cqrs/store" 8 | ) 9 | 10 | type MyModelSuite struct{} 11 | 12 | var _ = Suite(&MyModelSuite{}) 13 | 14 | func (s *MyModelSuite) TestBuilTransferWithNoEvents(c *C) { 15 | history := History{Events: []Event{}} 16 | transfer := Build(history) 17 | c.Assert(transfer, Equals, Transfer{}) 18 | } 19 | 20 | func (s *MyModelSuite) TestBuildCreatedTransfer(c *C) { 21 | transaction, _ := NewV4() 22 | from, _ := NewV4() 23 | to, _ := NewV4() 24 | events := []Event{TransferCreated{transaction, from, to, 100}} 25 | history := History{Events: events} 26 | transfer := Build(history) 27 | c.Assert(transfer.Uuid, Equals, transaction) 28 | c.Assert(transfer.From, Equals, from) 29 | c.Assert(transfer.To, Equals, to) 30 | c.Assert(transfer.Amount, Equals, 100) 31 | c.Assert(transfer.Status, Equals, Created) 32 | } 33 | 34 | func (s *MyModelSuite) TestBuildDebitedTransfer(c *C) { 35 | transaction, _ := NewV4() 36 | from, _ := NewV4() 37 | to, _ := NewV4() 38 | events := []Event{TransferCreated{transaction, from, to, 100}, TransferDebited{transaction, from, to, 100}} 39 | history := History{Events: events} 40 | transfer := Build(history) 41 | c.Assert(transfer.Status, Equals, Debited) 42 | } 43 | 44 | func (s *MyModelSuite) TestBuildCreditedNotDebitedTransfer(c *C) { 45 | transaction, _ := NewV4() 46 | from, _ := NewV4() 47 | to, _ := NewV4() 48 | events := []Event{TransferCreated{transaction, from, to, 100}, TransferCredited{transaction, from, to, 100}} 49 | history := History{Events: events} 50 | transfer := Build(history) 51 | c.Assert(transfer.Status, Equals, Created) 52 | } 53 | 54 | func (s *MyModelSuite) TestBuildDebitedAndCreditedTransfer(c *C) { 55 | transaction, _ := NewV4() 56 | from, _ := NewV4() 57 | to, _ := NewV4() 58 | events := make([]Event, 3) 59 | events[0] = TransferCreated{transaction, from, to, 100} 60 | events[1] = TransferDebited{transaction, from, to, 100} 61 | events[2] = TransferCredited{transaction, from, to, 100} 62 | history := History{Events: events} 63 | transfer := Build(history) 64 | c.Assert(transfer.Status, Equals, Completed) 65 | } 66 | 67 | func (s *MyModelSuite) TestBuildCreditedAndDebitedTransfer(c *C) { 68 | transaction, _ := NewV4() 69 | from, _ := NewV4() 70 | to, _ := NewV4() 71 | events := make([]Event, 3) 72 | events[0] = TransferCreated{transaction, from, to, 100} 73 | events[1] = TransferCredited{transaction, from, to, 100} 74 | events[2] = TransferDebited{transaction, from, to, 100} 75 | history := History{Events: events} 76 | transfer := Build(history) 77 | c.Assert(transfer.Status, Equals, Completed) 78 | } 79 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/transfer/service.go: -------------------------------------------------------------------------------- 1 | package transfer 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | "mcveat/cqrs/listener" 8 | . "mcveat/cqrs/store" 9 | ) 10 | 11 | type Service struct { 12 | store *EventStore 13 | } 14 | 15 | func NewService(es *EventStore) Service { 16 | return Service{es} 17 | } 18 | 19 | func (s *Service) Act(cmd Command) chan *UUID { 20 | done := make(chan *UUID) 21 | go s.act(cmd, done) 22 | return done 23 | } 24 | 25 | func (s *Service) act(cmd Command, done chan *UUID) { 26 | switch v := cmd.(type) { 27 | case CreateTransfer: 28 | event := TransferCreated{From: v.From, To: v.To, Amount: v.Amount} 29 | for result := range s.store.Save([]Event{event}) { 30 | done <- result 31 | } 32 | case Debite: 33 | event := TransferDebited{v.Uuid, v.From, v.To, v.Amount} 34 | done <- s.actionOnTransfer(v.Uuid, event) 35 | case Complete: 36 | event := TransferCredited{v.Uuid, v.From, v.To, v.Amount} 37 | done <- s.actionOnTransfer(v.Uuid, event) 38 | } 39 | close(done) 40 | } 41 | 42 | func (s *Service) actionOnTransfer(uuid *UUID, event Event) *UUID { 43 | account := <-s.store.Find(uuid) 44 | update := Update{uuid, []Event{event}, account.Version} 45 | err := <-s.store.Update(update) 46 | if err != nil { 47 | fmt.Println("Update on transfer failed", err) 48 | } 49 | return uuid 50 | } 51 | 52 | func (s *Service) StartListener() { 53 | go listener.Listen(s.store, s.handleEvent) 54 | } 55 | 56 | func (s *Service) handleEvent(e Event) chan *UUID { 57 | switch event := e.(type) { 58 | case AccountDebitedOnTransfer: 59 | return s.Act(Debite{event.Transaction, event.From, event.To, event.Amount}) 60 | case AccountCreditedOnTransfer: 61 | return s.Act(Complete{event.Transaction, event.From, event.To, event.Amount}) 62 | } 63 | return nil 64 | } 65 | 66 | func (s *Service) Find(uuid *UUID) chan Transfer { 67 | done := make(chan Transfer) 68 | go func() { 69 | done <- Build(<-s.store.Find(uuid)) 70 | close(done) 71 | }() 72 | return done 73 | } 74 | -------------------------------------------------------------------------------- /src/mcveat/cqrs/transfer/service_test.go: -------------------------------------------------------------------------------- 1 | package transfer 2 | 3 | import ( 4 | . "github.com/go-check/check" 5 | . "github.com/nu7hatch/gouuid" 6 | . "mcveat/cqrs/event" 7 | "mcveat/cqrs/store" 8 | "testing" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | TestingT(t) 13 | } 14 | 15 | type MySuite struct{} 16 | 17 | var _ = Suite(&MySuite{}) 18 | 19 | func (s *MySuite) TestCreateTransfer(c *C) { 20 | es := store.Empty() 21 | ts := Service{&es} 22 | from, _ := NewV4() 23 | to, _ := NewV4() 24 | uuid := <-ts.Act(CreateTransfer{from, to, 100}) 25 | page := <-es.Events(0, 10) 26 | c.Assert(page.Events, HasLen, 1) 27 | history := <-es.Find(uuid) 28 | c.Assert(history.Events, HasLen, 1) 29 | c.Assert(history.Events[0], Equals, TransferCreated{uuid, from, to, 100}) 30 | c.Assert(history.Version, Equals, 1) 31 | } 32 | 33 | func (s *MySuite) TestDebitAndCreditTransfer(c *C) { 34 | es := store.Empty() 35 | ts := Service{&es} 36 | from, _ := NewV4() 37 | to, _ := NewV4() 38 | uuid := <-ts.Act(CreateTransfer{from, to, 100}) 39 | <-ts.Act(Debite{uuid, from, to, 100}) 40 | <-ts.Act(Complete{uuid, from, to, 100}) 41 | page := <-es.Events(0, 10) 42 | c.Assert(page.Events, HasLen, 3) 43 | history := <-es.Find(uuid) 44 | c.Assert(history.Events, HasLen, 3) 45 | c.Assert(history.Events[0], Equals, TransferCreated{uuid, from, to, 100}) 46 | c.Assert(history.Events[1], Equals, TransferDebited{uuid, from, to, 100}) 47 | c.Assert(history.Events[2], Equals, TransferCredited{uuid, from, to, 100}) 48 | c.Assert(history.Version, Equals, 3) 49 | } 50 | 51 | func (s *MySuite) TestHandleAccountDebitedOnTransferEvent(c *C) { 52 | es := store.Empty() 53 | ts := Service{&es} 54 | from, _ := NewV4() 55 | to, _ := NewV4() 56 | 57 | uuid := <-ts.Act(CreateTransfer{from, to, 100}) 58 | <-ts.handleEvent(AccountDebitedOnTransfer{from, uuid, 100, from, to}) 59 | 60 | page := <-es.Events(0, 10) 61 | c.Assert(page.Events, HasLen, 2) 62 | history := <-es.Find(uuid) 63 | c.Assert(history.Events, HasLen, 2) 64 | c.Assert(history.Events[0], Equals, TransferCreated{uuid, from, to, 100}) 65 | c.Assert(history.Events[1], Equals, TransferDebited{uuid, from, to, 100}) 66 | c.Assert(history.Version, Equals, 2) 67 | } 68 | 69 | func (s *MySuite) TestHandleAccountDebitedAndCreditedOnTransferEvent(c *C) { 70 | es := store.Empty() 71 | ts := Service{&es} 72 | from, _ := NewV4() 73 | to, _ := NewV4() 74 | 75 | uuid := <-ts.Act(CreateTransfer{from, to, 100}) 76 | <-ts.handleEvent(AccountDebitedOnTransfer{from, uuid, 100, from, to}) 77 | <-ts.handleEvent(AccountCreditedOnTransfer{from, uuid, 100, from, to}) 78 | 79 | page := <-es.Events(0, 10) 80 | c.Assert(page.Events, HasLen, 3) 81 | history := <-es.Find(uuid) 82 | c.Assert(history.Events, HasLen, 3) 83 | c.Assert(history.Events[0], Equals, TransferCreated{uuid, from, to, 100}) 84 | c.Assert(history.Events[1], Equals, TransferDebited{uuid, from, to, 100}) 85 | c.Assert(history.Events[2], Equals, TransferCredited{uuid, from, to, 100}) 86 | c.Assert(history.Version, Equals, 3) 87 | } 88 | -------------------------------------------------------------------------------- /vendor/src/github.com/go-check/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcveat/event-sourcing-cqrs/72d1684201aa202e2e9f6535197b5fbcea28ccc5/vendor/src/github.com/go-check/.gitkeep -------------------------------------------------------------------------------- /vendor/src/github.com/nu7hatch/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcveat/event-sourcing-cqrs/72d1684201aa202e2e9f6535197b5fbcea28ccc5/vendor/src/github.com/nu7hatch/.gitkeep --------------------------------------------------------------------------------