event_sourcery 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +37 -0
- data/.rspec +3 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +82 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +399 -0
- data/Rakefile +6 -0
- data/bin/console +6 -0
- data/bin/setup +15 -0
- data/event_sourcery.gemspec +28 -0
- data/lib/event_sourcery.rb +49 -0
- data/lib/event_sourcery/aggregate_root.rb +68 -0
- data/lib/event_sourcery/config.rb +43 -0
- data/lib/event_sourcery/errors.rb +19 -0
- data/lib/event_sourcery/event.rb +49 -0
- data/lib/event_sourcery/event_body_serializer.rb +42 -0
- data/lib/event_sourcery/event_processing/error_handlers/constant_retry.rb +23 -0
- data/lib/event_sourcery/event_processing/error_handlers/error_handler.rb +20 -0
- data/lib/event_sourcery/event_processing/error_handlers/exponential_backoff_retry.rb +40 -0
- data/lib/event_sourcery/event_processing/error_handlers/no_retry.rb +19 -0
- data/lib/event_sourcery/event_processing/esp_process.rb +41 -0
- data/lib/event_sourcery/event_processing/esp_runner.rb +105 -0
- data/lib/event_sourcery/event_processing/event_stream_processor.rb +125 -0
- data/lib/event_sourcery/event_processing/event_stream_processor_registry.rb +29 -0
- data/lib/event_sourcery/event_store/each_by_range.rb +25 -0
- data/lib/event_sourcery/event_store/event_builder.rb +19 -0
- data/lib/event_sourcery/event_store/event_sink.rb +18 -0
- data/lib/event_sourcery/event_store/event_source.rb +21 -0
- data/lib/event_sourcery/event_store/event_type_serializers/class_name.rb +19 -0
- data/lib/event_sourcery/event_store/event_type_serializers/legacy.rb +17 -0
- data/lib/event_sourcery/event_store/event_type_serializers/underscored.rb +68 -0
- data/lib/event_sourcery/event_store/poll_waiter.rb +18 -0
- data/lib/event_sourcery/event_store/signal_handling_subscription_master.rb +22 -0
- data/lib/event_sourcery/event_store/subscription.rb +43 -0
- data/lib/event_sourcery/memory/event_store.rb +76 -0
- data/lib/event_sourcery/memory/tracker.rb +27 -0
- data/lib/event_sourcery/repository.rb +31 -0
- data/lib/event_sourcery/rspec/event_store_shared_examples.rb +352 -0
- data/lib/event_sourcery/version.rb +3 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5a53861c6b78ba25db797887cbb523bec700832a
|
4
|
+
data.tar.gz: 2cc44cb2db162bf4b1f35e356e24eab1c213534d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 77e1852829109a6fcab8e4c2efb930de89b88dfa45eccbecf761484c7a19f6ab8825fd9179c9112002e6c82ab281033106dc0da60fad2f8be76dd97e0c42d782
|
7
|
+
data.tar.gz: e0c6879eb49868144c628b6649c37d6577637f3514ef2653606a47d342cd820c9ed38f44588f77acd145f7b528a98b314a3c9189e27cd8f2971391c9ffe2978c
|
data/.gitignore
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Created by .ignore support plugin (hsz.mobi)
|
2
|
+
### Ruby template
|
3
|
+
*.gem
|
4
|
+
*.rbc
|
5
|
+
/.config
|
6
|
+
/coverage/
|
7
|
+
/InstalledFiles
|
8
|
+
/pkg/
|
9
|
+
/spec/reports/
|
10
|
+
/spec/examples.txt
|
11
|
+
/test/tmp/
|
12
|
+
/test/version_tmp/
|
13
|
+
/tmp/
|
14
|
+
|
15
|
+
# Used by dotenv library to load environment variables.
|
16
|
+
# .env
|
17
|
+
|
18
|
+
## Documentation cache and generated files:
|
19
|
+
/.yardoc/
|
20
|
+
/_yardoc/
|
21
|
+
/doc/
|
22
|
+
/rdoc/
|
23
|
+
|
24
|
+
## Environment normalization:
|
25
|
+
/.bundle/
|
26
|
+
/vendor/bundle
|
27
|
+
/lib/bundler/man/
|
28
|
+
|
29
|
+
# for a library or gem, you might want to ignore these files since the code is
|
30
|
+
# intended to run in multiple environments; otherwise, check them in:
|
31
|
+
Gemfile.lock
|
32
|
+
.ruby-version
|
33
|
+
.ruby-gemset
|
34
|
+
|
35
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
36
|
+
.rvmrc
|
37
|
+
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Change Log
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
### Added
|
9
|
+
- The core Event class accepts `causation_id` to allow event stores to
|
10
|
+
add support for tracking causation ids with events.
|
11
|
+
- The core Memory event store saves the `causation_id` and `correlation_id`.
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- The event store shared RSpec examples specify event stores should save
|
15
|
+
the `causation_id` and `correlation_id`.
|
16
|
+
|
17
|
+
### Removed
|
18
|
+
- The `processing_event` method from the memory tracker. It was intended to
|
19
|
+
be a mechanism to wrap processing and tracker updates which appears to be
|
20
|
+
universally unused at this point.
|
21
|
+
|
22
|
+
## [0.12.0] - 2017-6-1
|
23
|
+
### Removed
|
24
|
+
- Removed usage `#shutdown!` as it should be a private method within custom PollWaiters.
|
25
|
+
An example of how event_sourcery-postgres has implemented `#shutdown!` can be
|
26
|
+
found [here](https://github.com/envato/event_sourcery-postgres/pull/5)
|
27
|
+
|
28
|
+
## [0.11.2] - 2017-5-29
|
29
|
+
### Fixed
|
30
|
+
- Fixed: default poll waiter now implements `shutdown!`
|
31
|
+
|
32
|
+
## [0.11.0] - 2017-5-29
|
33
|
+
### Fixed
|
34
|
+
- Use `processor.class.name` to set ESP process name
|
35
|
+
- Convert `processor_name` symbol to string explicitly
|
36
|
+
|
37
|
+
## [0.11.0] - 2017-5-26
|
38
|
+
### Added
|
39
|
+
- Make Event processing error handler class Configurable
|
40
|
+
- Add exponential back off retry error handler
|
41
|
+
|
42
|
+
## [0.10.0] - 2017-5-24
|
43
|
+
### Added
|
44
|
+
- The core Event class accepts `correlation_id` to allow event stores to
|
45
|
+
add support for tracking correlation IDs with events.
|
46
|
+
- `Repository#save` for saving aggregate instances.
|
47
|
+
- Configuration option to define custom event body serializers.
|
48
|
+
|
49
|
+
### Fixed
|
50
|
+
- Resolved Sequel deprecation notice when loading events from the Postgres event
|
51
|
+
store.
|
52
|
+
|
53
|
+
### Changed
|
54
|
+
- Aggregates no longer save events directly to an event sink. They must be
|
55
|
+
passed back to the repository for saving with `repository.save(aggregate)`.
|
56
|
+
- `AggregateRoot#apply_event` signature has changed from accepting an event or
|
57
|
+
a hash to accepting an event class followed by what would normally go in the
|
58
|
+
constructor of the event.
|
59
|
+
|
60
|
+
### Removed
|
61
|
+
- Postgres specific code has moved to the [event_sourcery-postgres](https://github.com/envato/event_sourcery-postgres) gem.
|
62
|
+
Config options for postgres have moved to `EventSourcery::Postgres.config`.
|
63
|
+
|
64
|
+
## [0.9.0] - 2017-05-02
|
65
|
+
### Added
|
66
|
+
- Add `table_prefix` method to `TableOwner` to declare a table name prefix for
|
67
|
+
all tables in a projector or reactor.
|
68
|
+
|
69
|
+
### Changed
|
70
|
+
- Schema change: the `writeEvents` function has been refactored slightly.
|
71
|
+
- The `Event` class no longer uses `Virtus.value_object`.
|
72
|
+
- `AggregateRoot` and `Repository` are namespaced under `EventSourcery` instead
|
73
|
+
of `EventSourcery::Command`.
|
74
|
+
- `EventSourcery::Postgres` namespace has been extracted from
|
75
|
+
`EventSourcery::(EventStore|EventProcessing)::Postgres` in preparation for
|
76
|
+
moving all Postgres related code into a separate gem.
|
77
|
+
- An advisory lock has replaced the exclusive table lock used to synchronise
|
78
|
+
event inserts.
|
79
|
+
|
80
|
+
### Removed
|
81
|
+
- EventSourcery no longer depends on Virtus.
|
82
|
+
- `Command` and `CommandHandler` have been removed.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at odindutton@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: http://contributor-covenant.org
|
74
|
+
[version]: http://contributor-covenant.org/version/1/4/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Envato
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,399 @@
|
|
1
|
+
# EventSourcery
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/envato/event_sourcery.svg?branch=master)](https://travis-ci.org/envato/event_sourcery)
|
4
|
+
|
5
|
+
A framework for building event sourced, CQRS applications.
|
6
|
+
|
7
|
+
**Table of Contents**
|
8
|
+
|
9
|
+
- [Development Status](#development-status)
|
10
|
+
- [Goals](#goals)
|
11
|
+
- [Getting Started Guide](#getting-started-guide)
|
12
|
+
- [Configuration](#configuration)
|
13
|
+
- [Development](#development)
|
14
|
+
- [Dependencies](#dependencies)
|
15
|
+
- [Running the Test Suite](#running-the-test-suite)
|
16
|
+
- [Release](#release)
|
17
|
+
- [Core Concepts](#core-concepts)
|
18
|
+
- [Tour of an EventSourcery Web Application](#tour-of-an-eventsourcery-web-application)
|
19
|
+
- [Events](#events)
|
20
|
+
- [The Event Store](#the-event-store)
|
21
|
+
- [Storing Events](#storing-events)
|
22
|
+
- [Reading Events](#reading-events)
|
23
|
+
- [Aggregates and Command Handling](#aggregates-and-command-handling)
|
24
|
+
- [Event Processing](#event-processing)
|
25
|
+
- [Event Stream Processors](#event-stream-processors)
|
26
|
+
- [Projectors](#projectors)
|
27
|
+
- [Reactors](#reactors)
|
28
|
+
- [Running Multiple ESPs](#running-multiple-esps)
|
29
|
+
- [Typical Flow of State in an Event Sourcery Application](#typical-flow-of-state-in-an-event-sourcery-application)
|
30
|
+
- [1. Handling a Command](#1-handling-a-command)
|
31
|
+
- [2. Updating a Projection](#2-updating-a-projection)
|
32
|
+
- [3. Handling a Query](#3-handling-a-query)
|
33
|
+
|
34
|
+
## Development Status
|
35
|
+
|
36
|
+
EventSourcery is currently being used in production by multiple apps but we
|
37
|
+
haven't finalized the API yet and things are still moving rapidly. Until we
|
38
|
+
release a 1.0 things may change without first being deprecated.
|
39
|
+
|
40
|
+
## Goals
|
41
|
+
|
42
|
+
The goal of EventSourcery is to make it easier to build event sourced, CQRS applications.
|
43
|
+
|
44
|
+
The hope is that by using EventSourcery you can focus on modeling your domain with aggregates, commands, and events; and not worry about stitching together application plumbing.
|
45
|
+
|
46
|
+
## Getting Started Guide
|
47
|
+
|
48
|
+
EventSourcery currently supports a Postgres-based event store via the [event_sourcery-postgres](https://github.com/envato/event_sourcery-postgres) gem.
|
49
|
+
|
50
|
+
**TODO**
|
51
|
+
|
52
|
+
## Configuration
|
53
|
+
|
54
|
+
There are several ways to configure Event Sourcery to your liking. The following presents some examples:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
EventSourcery.configure do |config|
|
58
|
+
# Add custom reporting of errors occurring during event processing.
|
59
|
+
# One might set up Rollbar here.
|
60
|
+
config.on_event_processor_error = proc { |exception, processor_name| … }
|
61
|
+
|
62
|
+
# Enable Event Sourcery logging.
|
63
|
+
config.logger = Logger.new('logs/my_event_sourcery_app.log')
|
64
|
+
|
65
|
+
# Customize how event body attributes are serialized
|
66
|
+
config.event_body_serializer
|
67
|
+
.add(BigDecimal) { |decimal| decimal.to_s('F') }
|
68
|
+
|
69
|
+
# Config how your want to handle event processing errors
|
70
|
+
config.error_handler_class = EventSourcery::EventProcessing::ErrorHandlers::ExponentialBackoffRetry
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
### Dependencies
|
77
|
+
|
78
|
+
- Ruby
|
79
|
+
|
80
|
+
### Running the Test Suite
|
81
|
+
|
82
|
+
Run the `setup` script, inside the project directory to install the gem dependencies and create the test database (if it is not already created).
|
83
|
+
```bash
|
84
|
+
./bin/setup
|
85
|
+
```
|
86
|
+
|
87
|
+
Then you can run the test suite with rspec:
|
88
|
+
```bash
|
89
|
+
bundle exec rspec
|
90
|
+
```
|
91
|
+
|
92
|
+
### Release
|
93
|
+
|
94
|
+
To release a new version:
|
95
|
+
|
96
|
+
1. Update the version number in `lib/event_sourcery/version.rb`
|
97
|
+
2. Get this change onto master via the normal PR process
|
98
|
+
3. Run `bundle exec rake release`, this will create a git tag for the
|
99
|
+
version, push tags up to GitHub, and package the code in a `.gem` file.
|
100
|
+
|
101
|
+
## Core Concepts
|
102
|
+
|
103
|
+
Not sure what Event Sourcing (ES), Command Query Responsibility Segregation (CQRS), or even Domain-Driven Design (DDD) are? Here are a few links to get you started:
|
104
|
+
|
105
|
+
- [CQRS and Event Sourcing Talk](https://www.youtube.com/watch?v=JHGkaShoyNs) - by Greg Young at Code on the Beach 2014
|
106
|
+
- [DDD/CQRS Google Group](https://groups.google.com/forum/#!forum/dddcqrs) - from people new to the concepts to old hands
|
107
|
+
- [DDD Weekly Newsletter](https://buildplease.com/pages/dddweekly/) - a weekly digest of what's happening in the community
|
108
|
+
- [Domain-Driven Design](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) - the definitive guide
|
109
|
+
- [Greg Young's Blog](https://goodenoughsoftware.net) - a (the?) lead proponent of all things Event Sourcing
|
110
|
+
|
111
|
+
### Tour of an EventSourcery Web Application
|
112
|
+
|
113
|
+
Below is a high level view of a CQRS, event-sourced web application built using EventSourcery. The components marked with `*` can be created using building blocks provided by EventSourcery. Keep on reading and we'll describe each of the concepts illustrated.
|
114
|
+
|
115
|
+
```
|
116
|
+
┌─────────────┐ ┌─────────────┐
|
117
|
+
│ │ │ │
|
118
|
+
│ Client │ │ Client │
|
119
|
+
│ │ │ │
|
120
|
+
└─────────────┘ └─────────────┘
|
121
|
+
│ │
|
122
|
+
Issue Command Issue Query
|
123
|
+
│ │
|
124
|
+
┌───────┴──────────────────────────────────────┴─────────┐
|
125
|
+
│ Web Layer │
|
126
|
+
└───────┬──────────────────────────────────────┬─────────┘
|
127
|
+
│ │
|
128
|
+
▼ ▼
|
129
|
+
┌─────────────┐ ┌─────────────┐
|
130
|
+
│ Command │ │Query Handler│
|
131
|
+
│ Handler │ │ │
|
132
|
+
└─────────────┘ └─────────────┘
|
133
|
+
│ │
|
134
|
+
▼ ┌───────▼─────┐
|
135
|
+
┌─────────────┐ │┌────────────┴┐
|
136
|
+
│ * Aggregate │ ││* Projection │
|
137
|
+
│ │ └┤ │
|
138
|
+
└─────────────┘ └─────────────┘
|
139
|
+
│ ▲
|
140
|
+
│ │
|
141
|
+
│ Update Projection
|
142
|
+
│ │
|
143
|
+
Emit Event ┌─────────────┐
|
144
|
+
│ │┌────────────┴┐
|
145
|
+
│ ││ * Projector │
|
146
|
+
▼ └┤ │
|
147
|
+
┌─────────────┐ └─────────────┘
|
148
|
+
│* Event Store│ Process ▲
|
149
|
+
┌─▶│ │────────Event───────────────────┘
|
150
|
+
│ └─────────────┘
|
151
|
+
│ │
|
152
|
+
│ Process ┌─────────────┐
|
153
|
+
│ Event │┌────────────┴┐ ┌ ─ ─ ─ ─ ─ ─ ┐
|
154
|
+
│ └───────▶││ * Reactor │ External
|
155
|
+
│ └┤ │───Trigger ───▶│ System │
|
156
|
+
│ └─────────────┘ Behaviour ─ ─ ─ ─ ─ ─ ─
|
157
|
+
│ │
|
158
|
+
│ │
|
159
|
+
└────────Emit Event────────┘
|
160
|
+
|
161
|
+
```
|
162
|
+
|
163
|
+
### Events
|
164
|
+
|
165
|
+
Events are value objects that record something of meaning in the domain. Think of a sequence of events as a time series of immutable domain facts.
|
166
|
+
|
167
|
+
Events are targeted at an aggregate via an `aggregate_id` and have the following attributes.
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
module EventSourcery
|
171
|
+
class Event
|
172
|
+
attr_reader \
|
173
|
+
:id, # Sequence number
|
174
|
+
:uuid, # Unique ID
|
175
|
+
:aggregate_id, # ID of aggregate the event pertains to
|
176
|
+
:type, # type of the event
|
177
|
+
:body, # the payload (a hash)
|
178
|
+
:version, # Version of the aggregate
|
179
|
+
:created_at, # Created at date
|
180
|
+
:correlation_id # Correlation ID for tracing purposes
|
181
|
+
|
182
|
+
# ...
|
183
|
+
end
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
You can define events in your domain as follows.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
TodoAdded = Class.new(EventSourcery::Event)
|
191
|
+
|
192
|
+
# An example instance.
|
193
|
+
# #<TodoAdded:0x007fb6f88f04b0
|
194
|
+
# @id=24,
|
195
|
+
# @uuid="75dcc7eb-33c0-4f1c-ac23-31bf32fc5edc",
|
196
|
+
# @aggregate_id="fca315ff-d45d-46c5-a230-67c5bec0b06d",
|
197
|
+
# @type="todo_added",
|
198
|
+
# @body={"title"=>"My task"},
|
199
|
+
# @version=1,
|
200
|
+
# @created_at=2017-06-14 11:50:32 UTC,
|
201
|
+
# @correlation_id="b4d1e31d-9d1b-4ea1-a685-57936ce65a80">
|
202
|
+
```
|
203
|
+
|
204
|
+
### The Event Store
|
205
|
+
|
206
|
+
The event store is a persistent store of events.
|
207
|
+
|
208
|
+
EventSourcery currently supports a Postgres-based event store via the [event_sourcery-postgres gem](https://github.com/envato/event_sourcery-postgres).
|
209
|
+
|
210
|
+
For more information about the `EventStore` API refer to [the postgres event store](https://github.com/envato/event_sourcery-postgres/blob/master/lib/event_sourcery/postgres/event_store.rb) or the [in memory event store in this repo](lib/event_sourcery/event_store/memory.rb)
|
211
|
+
|
212
|
+
#### Storing Events
|
213
|
+
|
214
|
+
Naturally, it provides the ability to store events. The event store is append-only and immutable. The events in the store form a time-ordered sequence which can be viewed as a stream of events.
|
215
|
+
|
216
|
+
`EventStore` clients can optionally provide an expected version of event when saving to the store. This provides a mechanism for `EventStore` clients to effectively serialise the processing they perform against an instance of an aggregate.
|
217
|
+
|
218
|
+
When used in this fashion the event store can be thought of as an event sink.
|
219
|
+
|
220
|
+
#### Reading Events
|
221
|
+
|
222
|
+
The `EventStore` also allows clients to read events. Clients can poll the store for events of specific types after a specific event ID. They can also subscribe to the event store to be notified when new events are added to the event store that match the above criteria.
|
223
|
+
|
224
|
+
When used in this fashion the event store can be thought of as an event source.
|
225
|
+
|
226
|
+
### Aggregates and Command Handling
|
227
|
+
|
228
|
+
> An aggregate is a cluster of domain objects that can be treated as a single unit. Every transaction is scoped to a single aggregate. An aggregate will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole.
|
229
|
+
>
|
230
|
+
> <cite>— [DDD Aggregate](http://martinfowler.com/bliki/DDD_Aggregate.html)</cite>
|
231
|
+
|
232
|
+
Clients execute domain transactions against the system by issuing commands against aggregate roots. The result of these commands is new events being saved to the event store.
|
233
|
+
|
234
|
+
A typical EventSourcery application will have one or more aggregate roots with multiple commands.
|
235
|
+
|
236
|
+
### Event Processing
|
237
|
+
|
238
|
+
A central part of EventSourcery is the processing of events in the store. Event Sourcery provides the Event Stream Processor abstraction to support this.
|
239
|
+
|
240
|
+
```
|
241
|
+
┌─────────────┐ Subscribe to the event store
|
242
|
+
│Event Stream │ and take some action. Tracks
|
243
|
+
│ Processor │◀─ ─ ─ ─ ─its position in the stream in
|
244
|
+
│ │ a way that suits its needs.
|
245
|
+
└─────────────┘
|
246
|
+
▲
|
247
|
+
┌────────┴───────────┐
|
248
|
+
│ │
|
249
|
+
│ │
|
250
|
+
┌─────────────┐ ┌─────────────┐
|
251
|
+
Listens for events and takes │ │ │ │ Listens for events and
|
252
|
+
action. Actions include ─▶│ Reactor │ │ Projector │◀─ ┐ projects data into a
|
253
|
+
emitting new events into the ─ ┘ │ │ │ │ ─ ─ projection.
|
254
|
+
store and/or triggering side └─────────────┘ └─────────────┘
|
255
|
+
effects in the world.
|
256
|
+
```
|
257
|
+
|
258
|
+
A typical Event Sourcery application will have multiple projectors and reactors running as background processes.
|
259
|
+
|
260
|
+
#### Event Stream Processors
|
261
|
+
|
262
|
+
Event Stream Processors (ESPs) subscribe to an event store. They read events from the event store and take some action.
|
263
|
+
|
264
|
+
When newly created, an ESP will process the event stream from the beginning. When catching up like this an ESP can process events in batches (currently set to 1,000 events). This allows them to optimise processing as desired.
|
265
|
+
|
266
|
+
ESPs track the position in the event stream that they've processed in a way that suits them. This allows for them to optimise transaction handling in the case where they are catching up for example.
|
267
|
+
|
268
|
+
#### Projectors
|
269
|
+
|
270
|
+
A Projector is an EventStreamProcessor that listens for events and projects data into a projection. These projections are generally consumed on the read side of the CQRS world.
|
271
|
+
|
272
|
+
Projectors tend to be built for specific read-side needs and are generally specific to a single read case.
|
273
|
+
|
274
|
+
Modifying a projection is achieved by creating a new projector.
|
275
|
+
|
276
|
+
#### Reactors
|
277
|
+
|
278
|
+
A Reactor is an EventStreamProcessor that listens to events and emits events back into the store and/or trigger side effects in the world.
|
279
|
+
|
280
|
+
They typically record any external side effects they've triggered as events in the store.
|
281
|
+
|
282
|
+
#### Running Multiple ESPs
|
283
|
+
|
284
|
+
An EventSourcery application will typically have multiple ESPs running. EventSourcery provides a class called [ESPRunner](lib/event_sourcery/event_processing/esp_runner.rb) which can be used to run ESPs. It runs each ESP in a forked child process so each ESP can process the event store independently. You can find an example in [event_sourcery_todo_app](https://github.com/envato/event_sourcery_todo_app/blob/master/Rakefile).
|
285
|
+
|
286
|
+
Note that you may instead choose to run each ESP in their own process directly. The coordination of this is not currently provided by EventSourcery.
|
287
|
+
|
288
|
+
### Typical Flow of State in an Event Sourcery Application
|
289
|
+
|
290
|
+
Below we see the typical flow of state in an Event Sourcery application (arrows indicate data flow). Note that steps 1 and 2 are not synchronous. This means Event Sourcery applications need to embrace [eventual consistency](https://en.wikipedia.org/wiki/Eventual_consistency).
|
291
|
+
|
292
|
+
```
|
293
|
+
|
294
|
+
1. Issue Command │ 2. Update Projection │ 3. Issue Query
|
295
|
+
|
296
|
+
│ │
|
297
|
+
│ ▲
|
298
|
+
│ │ │ │
|
299
|
+
│ │
|
300
|
+
│ │ │ F. Handle
|
301
|
+
B. Handle Query
|
302
|
+
Command │ │ │
|
303
|
+
│ │
|
304
|
+
│ │ │ │
|
305
|
+
▼ ┌─────────────┐ │
|
306
|
+
┌─────────────┐ │ │ │ │ ┌─────────────┐
|
307
|
+
│ │ ┌───────▶│ Projector │ │ │
|
308
|
+
┌─▶│ Aggregate │ │ │ │ │ │ │Query Handler│
|
309
|
+
│ │ │ │ └─────────────┘ │ │
|
310
|
+
│ └─────────────┘ │ D. Read │ │ └─────────────┘
|
311
|
+
│ │ event E. Update ▲
|
312
|
+
A. Load C. Emit │ │ Projection │ │
|
313
|
+
state from Event │ │ G. Read
|
314
|
+
events │ │ │ │ │ Projection
|
315
|
+
│ ▼ │ ▼ │
|
316
|
+
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │
|
317
|
+
│ │ │ │ │ │ │
|
318
|
+
└──│ Event Store │───┼───┘ │ Projection │────┼───────────────┘
|
319
|
+
│ │ │ │
|
320
|
+
└─────────────┘ │ └─────────────┘ │
|
321
|
+
|
322
|
+
│ │
|
323
|
+
|
324
|
+
```
|
325
|
+
|
326
|
+
#### 1. Handling a Command
|
327
|
+
|
328
|
+
A command comes into the application and is routed to a command handler. The command handler initialises an aggregate and loads up its state from events in the store. The command handler then defers to the aggregate to handle the command. It then stores any new events raised by the aggregate into the event store.
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
class AddTodoCommandHandler
|
332
|
+
def handle(id:, title:, description:)
|
333
|
+
# The repository provides access to the event store for saving and loading aggregates
|
334
|
+
repository = EventSourcery::Repository.new(
|
335
|
+
event_source: EventSourcery.config.event_source,
|
336
|
+
event_sink: EventSourcery.config.event_sink,
|
337
|
+
)
|
338
|
+
|
339
|
+
# Load up the aggregate from events in the store
|
340
|
+
aggregate = repository.load(TodoAggregate, id)
|
341
|
+
|
342
|
+
# Defer to the aggregate to execute the add command.
|
343
|
+
# This may raise new events in the aggregate which we'll need to save.
|
344
|
+
aggregate.add(title, description)
|
345
|
+
|
346
|
+
# Save any newly raised events back into the event store
|
347
|
+
repository.save(aggregate)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
```
|
351
|
+
|
352
|
+
#### 2. Updating a Projection
|
353
|
+
|
354
|
+
Projecting is process of converting (or collecting) a stream of events into a structural representation. You can think of the process as a fold over a sequence of events. You can think of a projection as a read model that is generally persisted somewhere like a database table.
|
355
|
+
|
356
|
+
A projector is a process that listens for new events in the event store. When it sees a new event it cares about it updates its projection.
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
class OutstandingTodosProjector
|
360
|
+
include EventSourcery::Postgres::Projector
|
361
|
+
|
362
|
+
projector_name :outstanding_todos
|
363
|
+
|
364
|
+
# Define our database table projection
|
365
|
+
table :outstanding_todos do
|
366
|
+
column :todo_id, 'UUID NOT NULL'
|
367
|
+
column :title, :text
|
368
|
+
column :description, :text
|
369
|
+
end
|
370
|
+
|
371
|
+
# Handle TodoAdded events by adding the todo to our projection
|
372
|
+
project TodoAdded do |event|
|
373
|
+
table.insert(
|
374
|
+
todo_id: event.aggregate_id,
|
375
|
+
title: event.body['title'],
|
376
|
+
description: event.body['description'],
|
377
|
+
)
|
378
|
+
end
|
379
|
+
|
380
|
+
# Handle TodoCompleted events by removing the todo from our projection
|
381
|
+
project TodoCompleted, TodoAbandoned do |event|
|
382
|
+
table.where(todo_id: event.aggregate_id).delete
|
383
|
+
end
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
#### 3. Handling a Query
|
388
|
+
|
389
|
+
A query comes into the application and is routed to a query handler. The query handler queries the projection directly and returns the result.
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
module OutstandingTodos
|
393
|
+
class QueryHandler
|
394
|
+
def handle
|
395
|
+
EventSourceryTodoApp.projections_database[:outstanding_todos].all
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
```
|