Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/isalevine/event-sourcing-user-app
dev.to demo code for Event Sourcing system built around a User model
https://github.com/isalevine/event-sourcing-user-app
Last synced: 4 days ago
JSON representation
dev.to demo code for Event Sourcing system built around a User model
- Host: GitHub
- URL: https://github.com/isalevine/event-sourcing-user-app
- Owner: isalevine
- Created: 2020-05-02T02:45:44.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2023-01-19T18:41:23.000Z (almost 2 years ago)
- Last Synced: 2024-07-26T23:46:15.926Z (3 months ago)
- Language: Ruby
- Size: 1.1 MB
- Stars: 9
- Watchers: 2
- Forks: 1
- Open Issues: 33
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- awesome-ccamel - isalevine/event-sourcing-user-app - dev.to demo code for Event Sourcing system built around a User model (Ruby)
README
# This README is the tutorial to create an Event Sourcing system in Ruby on Rails.
The article can be found on Dev.to here:
[https://dev.to/isalevine/building-an-event-sourcing-system-in-rails-part-2-building-our-event-pattern-from-scratch-168p-temp-slug-1608448?preview=2640d0e9af2c0b95d6c79592e33429c8604aa7b6b44734ca55e33ca5c19942669fe6b9913cc71b333787bee60dca1f7993d03a9a2e78455a554b3d3c](https://dev.to/isalevine/building-an-event-sourcing-system-in-rails-part-2-building-our-event-pattern-from-scratch-168p-temp-slug-1608448?preview=2640d0e9af2c0b95d6c79592e33429c8604aa7b6b44734ca55e33ca5c19942669fe6b9913cc71b333787bee60dca1f7993d03a9a2e78455a554b3d3c)# To Recap: What is Event Sourcing?
Event Sourcing is a system design pattern that emphasizes recording changes to data via immutable events.
In other words: *every time your data changes*, you save an event to your database with the details.
**Those events never change or go away.** That way, you have a permanent, unchanging history of how your data reached its current state!
# What this article covers
We will primarily be working off of [Kickstarter's event sourcing example.](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224)To create our Event pattern, we’ll take the following steps:
- Get our Rails app up and running
- `User` model and controller
- PostgreSQL for our database
- Set up our environment to test our Events
- Postico to inspect our database
- Insomnia for REST client
- Add our Events pattern
- What is an Event, and what Event data will we store in the database?
- The BaseEvent that other Event classes will inherit from
- `Events::User::Created`
- `Events::User::Destroyed`# Getting our Rails app up and running
## Let’s go ahead and create our new Rails app
We’ll set the database to PostgreSQL with `--database=postgresql` and skip tests with `--skip-test`, as we will be adding RSpec manually later.
```ruby
rails new event-sourcing-user-app --database=postgresql --skip-test
```## Let’s add our `User` model
Our `User` model will have several fields:
- `name` String,
- `email` String,
- `password_digest` String (for bcrypt)
- `deleted` Boolean (remember, part of event sourcing is that we **never delete data**—instead, we will flag certain Users as _being_ deleted, and scope our queries appropriately)
- this field also needs to be `null: false`, and be set to `default: false`We’ll start this with Rails one-liner:
```ruby
rails g model User name email password_digest deleted:boolean
```And inside the new migration, tweak the `t.boolean :deleted` to be `null: false` and `default: false`:
```ruby
# db/migrate/20200502025357_create_users.rbclass CreateUsers < ActiveRecord::Migration[6.0]
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.boolean :deleted, null: false, default: falset.timestamps
end
end
end
```## Add a `User` controller and routes
Our User controller needs to have two actions, a `create` and `destroy` action, to handle the Events we want to make.Let’s create our controller manually, since we don’t need any views to be generated. In `app/controllers`, create a `users_controller` and add `def create` and `def destroy` actions:
```ruby
# app/controllers/users_controller.rbclass UsersController < ApplicationController
def create
enddef destroy
end
end
```Since we are not implementing auth yet, we’ll also add a `skip_before_action` hook to make testing our code easier:
```ruby
# app/controllers/users_controller.rbclass UsersController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create, :destroy]def create
enddef destroy
end
end
```Next, let’s manually add a POST and DELETE route that will go to the `create` and `destroy` actions in our controller:
```ruby
# config/routes.rbRails.application.routes.draw do
post 'users/create', to: 'users#create'
delete 'users/destroy', to: 'users#destroy'
end
```Run `rails routes` in your console to see that the routes are set up correctly:
```
[13:29:44] (master) event-sourcing-user-app
// ♥ rails routes
Prefix Verb URI Pattern Controller#Action
users_create POST /users/create(.:format) users#create
users_destroy DELETE /users/destroy(.:format) users#destroy
```## Run database migrations
Now, let’s create our databases and run our migrations in the usual two-step:
```ruby
rails db:create
rails db:migrate
```# Setting up our environment to test our Events
## Set up Postico to view our PostgreSQL database
If you’re not familiar with [Postico](https://eggerapps.at/postico/), it’s a a database management tool and viewer with a great free trial.Download and install from their website, and open it up. Go ahead and hit `Connect` to in the `localhost` using its default settings:
![Postico's main page](https://dev-to-uploads.s3.amazonaws.com/i/3e01npyfqoi9iwb84i2t.png)
From here, click the `localhost` button at the top to go to a list of available databases:
![Postico landing page inside localhost](https://dev-to-uploads.s3.amazonaws.com/i/ckp52em08tqocdr1yno6.png)
And now, we should be able to select our development database:
![Postico page listing available databases](https://dev-to-uploads.s3.amazonaws.com/i/t7cm9r4lp4el3va6mms3.png)
Select our `users` table:
![Postico page inside development database, showing users table](https://dev-to-uploads.s3.amazonaws.com/i/sj9trljc4a6wubgx7fcj.png)
And, hurray—there’s our User model, with it’s four fields:
![Postico users table](https://dev-to-uploads.s3.amazonaws.com/i/ge2z2vqt53m56cgxbvi0.png)
## Set up Insomnia to send HTTP requests
Likewise, if you’re not familiar with [Insomnia](https://insomnia.rest/), it’s a tool for sending HTTP requests to test RESTful APIs. We’ll be using **Insomnia Core**.Download, install, and open it up:
![Insomnia Core main page](https://dev-to-uploads.s3.amazonaws.com/i/7fku2239n0kymgfgl0lv.png)
Create a folder for our project, `event-sourcing-user-app`:
![Insomnia showing new project folder](https://dev-to-uploads.s3.amazonaws.com/i/388jrxxujj7n5e52fs06.png)
Let’s create our first request. We’ll make it a POST request, which we’ll user for a **create User** route:
![Insomnia showing new request being set to POST](https://dev-to-uploads.s3.amazonaws.com/i/khu76n08h5ubparkc8yk.png)
And lastly, we’ll set the target URL to `localhost:3000/users/create` for testing later:
![Insomnia showing target URL for Create User request](https://dev-to-uploads.s3.amazonaws.com/i/uu41v34shfies4g4dnju.png)
Yay, now Insomnia’s ready to go! We’ll just need to fill out the body of our request with a hash once we have our Events created.
## Testing the `create` action with `byebug` and Insomnia
You can test out the routes by adding a `byebug` to the controller action:
```ruby
# app/controllers/users_controller.rbdef create
byebug
end
```Fire up `rails s`, and send a POST request to `localhost:3000/users/create` in Insomnia. In your console, you will see `byebug` session:
![screenshot showing Insomnia request, and console inside a byebug session](https://dev-to-uploads.s3.amazonaws.com/i/6aywcx4o8j104jdyshq1.png)
Great, we can see our route working as expected!
**Now, we’re ready to build our event pattern!**
# What is an Event?
In our event sourcing system, each Event will be **a Rails model that stores information about changes to our data**.Our goal is to build two events:
- `Events::User::Created` — this will record:
- `payload`: a hash containing the `name`, `email`, and `password` params to create the User
- `user_id`: the created User’s `id`, used as a `belongs_to` relationship
- `event_type`: a String to show that this `user_event` is the `”Created”` type
- timestamps
- `Events::User::Destroyed` — this will record:
- `payload`: a hash containing the `id` for the User to be flagged as deleted
- `user_id`: the target User’s `id`, used as a `belongs_to` relationship
- `event_type`: a String to show that this `user_event` is the `”Destroyed”` type
- timestampsWhen our Rails app creates or destroys a User, this will also trigger creating a new Event.
These events will be saved to our database, and will be **immutable** to serve as a permanent log of changes.
Since we might end up having _a lot_ of User-related events, we’re also including the `event_type` field on our User events so we can store them all in one `user_events` table—and easily add add more later!
# The `Events::BaseEvent`
Our events will be built through inheritance. At the top of the chain, we will define `Events::BaseEvent` where a lot of the event functionality will live.Since all of our events will be Rails models, go ahead and create a new `/events` directory inside `app/models`.
Now, we can create our BaseEvent:
```ruby
# app/models/events/base_event.rbclass Events::BaseEvent < ActiveRecord::Base
end
```## `abstract_class`
Since the BaseEvent only exists for inheritance, we can make it an `abstract_class` so Rails knows not to try to load any records for it:
```ruby
# app/models/events/base_event.rbclass Events::BaseEvent < ActiveRecord::Base
self.abstract_class = true
end
```## `apply(aggregate)` and `apply_and_persist`
Each event will have to define its own `apply` method. This method will accept an `aggregate`—another model, in our case a User—and update its attributes.
_(The term `aggregate` comes from the Kickstarter event sourcing system, and [you can read more about it here](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224). Basically, `aggregates` are models that receive changes via `events`.)_On BaseEvent, we’ll simply raise a `NotImplementedError`. This will enforce us having to define it explicitly on each event, thus overriding the error via inheritance.
The BaseEvent will also have a `before_create` hook that calls `apply_and_persist`. This will call `apply`, then `save!` the update to the database.
_(It will also set the event’s `aggregate_id`, specifically for Created events where the `id` doesn’t exist until after `save!` is called.)_Let’s look at the code we’ll add:
```ruby
# app/models/events/base_event.rbbefore_create :apply_and_persist
def apply(aggregate)
raise NotImplementedError
endprivate def apply_and_persist
# Lock the database row! (OK because we're in an ActiveRecord callback chain transaction)
aggregate.lock! if aggregate.persisted?# Apply!
self.aggregate = apply(aggregate)#Persist!
aggregate.save!# Update aggregate_id with id from newly created User
self.aggregate_id = aggregate.id if aggregate_id.nil?
end
```## `after_initialize` and `event_type`
No matter what kind of event we instantiate, there are a couple attributes we want to set right away:
- `event_type` — every Event needs to be explicitly categorized for when it’s stored in the `user_events` table as a `BaseEvent` record
- `payload` — since we always expect `payload` to be accessible as a hash (and stored in our PostgreSQL database as JSON), we’ll add a `||` clause to set it to `{}` if the event accepts no paramsSo, we’ll add an `after_initialize` hook to set those attributes:
```ruby
# app/models/events/base_event.rbafter_initialize do
self.event_type = event_type
self.payload ||= {}
enddef event_type
self.attributes["event_type"] || self.class.to_s.split("::").last
end
```Above, we define `event_type` to quickly access its own type via `attributes` if loaded from our database—or upon first creation, deducing its type from the Event class’s name.
## `self.payload_attributes(*attributes)`
In each Event class we create, we want the option to define possible `payload_attributes` we want to record.On BaseEvent, `self.payload_attributes` will create the getters and setters for our payload fields:
```ruby
# app/models/events/base_event.rbdef self.payload_attributes(*attributes)
@payload_attributes ||= []attributes.map(&:to_s).each do |attribute|
@payload_attributes << attribute unless @payload_attributes.include?(attribute)define_method attribute do
self.payload ||= {}
self.payload[attribute]
enddefine_method "#{attribute}=" do |argument|
self.payload ||= {}
self.payload[attribute] = argument
end
end@payload_attributes
end
```Ultimately, this will let us define attributes like this at the top of each new Event class: `payload_attributes :name, :email, :password`
## `find_or_build_aggregate`
We want our events to be aware of their aggregates—in our case, the target User—and be able to either look it up, or create a new one.We’ll add a `before_validation` hook (which gets called _really early_ in the `.create` lifecycle) which will either look up or create the aggregate, based on whether an `id` is present in the event’s params:
```ruby
# app/models/events/base_event.rbbefore_validation :find_or_build_aggregate
private def find_or_build_aggregate
self.aggregate = find_aggregate if aggregate_id.present?
self.aggregate = build_aggregate if self.aggregate.nil?
enddef find_aggregate
klass = aggregate_name.to_s.classify.constantize
klass.find(aggregate_id)
enddef build_aggregate
public_send "build_#{aggregate_name}"
end
```Let’s see the code:
## `aggregate` setters, getters, and get-its-namers
To round out our events’ functionality, we’ll want some setters and getters—as well as methods to easily return its type or class name:
- `aggregate=(model)` and `aggregate` will set and get the User our event targets
- `aggregate_id=(id)` and `aggregate_id` will map to the `user_id` field on our `user_events` table
- `self.aggregate_name` gives the Event class awareness of its `belongs_to` relationship’s target class (`#=> User`)
- `delegate :aggregate_name, to: :class` will return a Symbol of the aggregate’s class name (`#=> :user`)
- `def event_klass` will convert our Event class’s `::BaseEvent` namespace into its appropriate event type (`#=> Events::User::Created`)```ruby
# app/models/events/base_event.rbdef aggregate=(model)
public_send "#{aggregate_name}=", model
end# Return the aggregate record that the event will apply to
def aggregate
public_send aggregate_name
enddef aggregate_id=(id)
public_send "#{aggregate_name}_id=", id
enddef aggregate_id
public_send "#{aggregate_name}_id"
enddef self.aggregate_name
inferred_aggregate = reflect_on_all_associations(:belongs_to).first
raise "Events must belong to an aggregate" if inferred_aggregate.nil?
inferred_aggregate.name
enddelegate :aggregate_name, to: :class
def event_klass
klass = self.class.to_s.split("::")
klass[-1] = event_type
klass.join('::').constantize
end
```## Okay, let’s see the whole `Events::BaseEvent`!
```ruby
# app/models/events/base_event.rb# Kickstarter code reference:
# https://github.com/pcreux/event-sourcing-rails-todo-app-demo/blob/master/app/models/lib/base_event.rbclass Events::BaseEvent < ActiveRecord::Base
before_validation :find_or_build_aggregate
before_create :apply_and_persistself.abstract_class = true
def apply(aggregate)
raise NotImplementedError
endafter_initialize do
self.event_type = event_type
self.payload ||= {}
enddef self.payload_attributes(*attributes)
@payload_attributes ||= []attributes.map(&:to_s).each do |attribute|
@payload_attributes << attribute unless @payload_attributes.include?(attribute)define_method attribute do
self.payload ||= {}
self.payload[attribute]
enddefine_method "#{attribute}=" do |argument|
self.payload ||= {}
self.payload[attribute] = argument
end
end@payload_attributes
endprivate def find_or_build_aggregate
self.aggregate = find_aggregate if aggregate_id.present?
self.aggregate = build_aggregate if self.aggregate.nil?
enddef find_aggregate
klass = aggregate_name.to_s.classify.constantize
klass.find(aggregate_id)
enddef build_aggregate
public_send "build_#{aggregate_name}"
endprivate def apply_and_persist
# Lock the database row! (OK because we're in an ActiveRecord callback chain transaction)
aggregate.lock! if aggregate.persisted?# Apply!
self.aggregate = apply(aggregate)#Persist!
aggregate.save!
self.aggregate_id = aggregate.id if aggregate_id.nil?
enddef aggregate=(model)
public_send "#{aggregate_name}=", model
enddef aggregate
public_send aggregate_name
enddef aggregate_id=(id)
public_send "#{aggregate_name}_id=", id
enddef aggregate_id
public_send "#{aggregate_name}_id"
enddef self.aggregate_name
inferred_aggregate = reflect_on_all_associations(:belongs_to).first
raise "Events must belong to an aggregate" if inferred_aggregate.nil?
inferred_aggregate.name
enddelegate :aggregate_name, to: :class
def event_type
self.attributes["event_type"] || self.class.to_s.split("::").last
enddef event_klass
klass = self.class.to_s.split("::")
klass[-1] = event_type
klass.join('::').constantize
endend
```# The `user_events` table, and the `Events::User::BaseEvent`
We previously mentioned that we will be storing multiple types of User-related events in a single `user_events` table.To accomplish this and allow us to easily add more events later, we will create an `Events::User::BaseEvent` which will tell all events in the `Events::User::` namespace to save to the `user_events` table. We will also define a `belongs_to` relationship with a User here.
## `user_events` table
Let’s go ahead and create our `user_events` table in our database.[Kickstarter’s event sourcing example](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224) describes that each `aggregate` (User) has an event table (`user_events`). These event tables will have a similar schema—we will tweak them slightly to match our verbiage:
> Each Aggregate (ex: subscriptions) has an Event table associated to it (ex: subscription_events).
> …
> All events related to an aggregate are stored in the same table. All events tables have a similar schema:
> `id, aggregate_id, type, data (json), metadata (json), created_at`A few things we’ll tweak for our code:
- `aggregate_id` will be replaced by `user_id`
- `type` will be replaced by `event_type` (just to be more explicit)
- `data` will be replaced by `payload`, and will still be type JSON
- `metadata` will not be included at this time, since our events are relatively simple
- `created_at` will not be included, since we will simply rely on ActiveRecord’s default timestampsWe will create our `user_events` table with a Rails migration:
```ruby
rails g migration CreateUserEvents
```This will create our migration with a `create_table` block set up for us:
```ruby
# db/migrate/20200502192018_create_user_events.rbclass CreateUserEvents < ActiveRecord::Migration[6.0]
def change
create_table :user_events do |t|
end
end
end
```We want to add four fields:
- a `belongs_to` relationship to a `:user`
- an `event_type` String
- a `payload` JSON
- timestamps```ruby
# db/migrate/20200502192018_create_user_events.rbclass CreateUserEvents < ActiveRecord::Migration[6.0]
def change
create_table :user_events do |t|
t.belongs_to :user, null: false, foreign_key: true
t.string :event_type
t.json :payloadt.timestamps
end
end
end
```Run the migration:
```ruby
rails db:migrate
```And open up Postico to check out the new `user_events` table:
![Postico page showing the user_events table selected](https://dev-to-uploads.s3.amazonaws.com/i/q90r3lfnvvnir8dgh1sa.png)
Our table and fields are ready to go!
![Postico page showing user_events table fields](https://dev-to-uploads.s3.amazonaws.com/i/wrmi6ouzhth65g4mte3p.png)
## `Events::User::BaseEvent`
Inside our `app/models/events` directory, create a new `user` directory.Inside that directory, create a new file `base_event.rb`. This gives us the namespacing to create this class:
```ruby
# app/models/events/user/base_event.rbclass Events::User::BaseEvent < Events::BaseEvent
self.table_name = "user_events"
end
```With `self.table_name = “user_events”`, any new Event class we create that inherits from `Events::User::BaseEvent` will automatically be saved and retrieved from the `user_events` table!
## `belongs_to :user` and `has_many :events`
Since all our User-related events target a User, it makes sense to create a `has_many / belongs_to` relationship between Users and Events in the `Events::User::` namespace.Since we’re deep in a namespace that uses the name `User`, to tell Rails to look for the regular top-level `User` model, we need to add `::` before our classnames. This tells our `has_many` and `belongs_to` relationships to look outside the current namespace.
Let’s update our `Events::User::BaseEvent` and `User` classes with the relationships:
```ruby
# app/models/events/user/base_event.rbclass Events::User::BaseEvent < Events::BaseEvent
self.table_name = "user_events"belongs_to :user, class_name: "::User"
end# app/models/user.rb
class User < ApplicationRecord
has_many :events, class_name: "Events::User::BaseEvent"
end
```Great! Now, when we load a User into a `user` variable, we can call `user.events` to load all related events from the `user_events` table.
**We’re now ready to create some real, _usable_ Events!**
# Creating a new User with `Events::User::Created`
With our BaseEvent pattern in place, we can now build our first event!`Events::User::Created` will record the params used to create a User, as well as the new User’s id, and the event’s timestamp.
## Build the `Events::User::Created` class
In `app/models/events/user`, make a new `created.rb` file. Our class will inherit from `Events::User::BaseEvent` in the same directory:
```ruby
# app/models/events/user/created.rbclass Events::User::Created < Events::User::BaseEvent
end
```As we defined in the top-level `Events::BaseEvent`, we must define an `apply` method that will take a User instance as its `aggregate` argument:
```ruby
# app/models/events/user/created.rbclass Events::User::Created < Events::User::BaseEvent
def apply(user)
end
end
```Since we know creating a User requires params with a `name`, `email`, and `password`, we can also add them as a list of symbols to `payload_attributes` to create our getters and setters:
```ruby
# app/models/events/user/created.rbclass Events::User::Created < Events::User::BaseEvent
payload_attributes :name, :email, :passworddef apply(user)
end
end
```## Add logic to the `apply` method
The logic in the event’s `apply` method is where the event’s power lies. It:
- takes in a User instance
- applies the changes to the User instance, supplied by `payload_attributes`
- returns the mutated User instance => **this is where the top-level BaseEvent receives back the User instance, and calls `save!` to persist the changes in the database!**Thanks to the list of attributes passed to `payload_attributes`, we can simply call the attributes inside our `apply` method to update the User instance:
```ruby
# app/models/events/user/created.rbpayload_attributes :name, :email, :password
def apply(user)
user.name = name
user.email = email
user.password_digest = passworduser
end
```Perfect! Now, all we need to do is tell Insomnia to pass params that contain `name`, `email`, and `password` Strings, and our event will map them to the User model’s `name`, `email`, and `password_digest` fields.
_(Remember: `password_digest` is related to `bcrypt` functionality, which we will explore in another article.)_## Update our controller to create an Event and use strong params
Back in our `users_controller`, we need to update two things:
- the `create` action needs to call `Events::User::Created.create(payload: user_params)`
- add strong params to protect the `user_params` we will pass to `.create(payload: user_params)`For the strong params, we will require the `user_params` to have `name`, `email`, and `password` nested inside a `user` key:
```ruby
# app/controllers/users_controller.rbprivate def user_params
params.require(:user).permit(:name, :email, :password)
end
```Now, we can safely pass `user_params` to `Events::User::Created.create(payload: user_params)` in the `create` action:
```ruby
# app/controllers/users_controller.rbdef create
Events::User::Created.create(payload: user_params)
endprivate def user_params
params.require(:user).permit(:name, :email, :password)
end
```## Let’s test our event with Insomnia and Postico!
If we send the correct params via a POST request to `localhost:3000/users/create`, we expect several behaviors:
- a new record in the `user_events` table, with:
- `event_type “Created”`
- `payload` with the `user_params`
- note that the `password` will be stored as plaintext => **this is UNSAFE BEHAVIOR, and is because we have not implemented bcrypt encryption yet!**
- `user_id` with the newly-created User’s `id`
- a new record in the `user` table, with:
- correct `name`
- correct `email`
- `password_digest` that is the plaintext `password` => **this is UNSAFE BEHAVIOR, and is because we have not implemented bcrypt encryption yet!**Let’s test it out!
Fire up `rails s`, and open up Insomnia.
In our `Create User` request, set the Body to JSON:
![Insomnia page showing Body type being set to JSON](https://dev-to-uploads.s3.amazonaws.com/i/g3rgthzj73uz2dvhn37w.png)
Then, create a JSON hash with a `”user”` key, which points to a hash containing a `”name”`, `”email”`, and `”password”`:
![Insomnia page showing JSON body with user params](https://dev-to-uploads.s3.amazonaws.com/i/dcq61uzaow04u1m6bc7j.png)
Now hit `Send`, and let’s check out our database tables!
First, let’s see if we have an event in our `user_events` table:
![Postico table with first Created event record, overlaid on Insomnia request body](https://dev-to-uploads.s3.amazonaws.com/i/acge8hrnkrq3vm4zsper.png)
So far, so good!
_(Remember: **storing passwords as plaintext is UNSAFE BEHAVIOR, and is because we have not implemented bcrypt encryption yet!**)_Now, let’s check out the `users` table:
![Postico table with first User record, overlaid on Insomnia request body](https://dev-to-uploads.s3.amazonaws.com/i/d82ai0lxwbhoi50sdvhh.png)
Terrific! We now have our new User, `ongo_gablogian`, and a record of the Event and params that created him!
![gif of Danny DeVito as Ongo Gablogian, a parody of Andy Warhol, on Always Sunny](https://dev-to-uploads.s3.amazonaws.com/i/cl772dunyxs83v8cjb24.gif)
**There you have it! Our event sourcing system is now capturing changes to our data!**
As long as we never alter the data in the `user_events` table, we have a reliable log of how our data got to its current state!
![screenshot of a banner stating MISSION ACCOMPLISHED on Arrested Development](https://dev-to-uploads.s3.amazonaws.com/i/ls69lrx60hiimpan2qpv.jpeg)
# Destroying a User with `Events::User::Destroyed`
Now that we have our pattern in place, it’s very straightforward to create a new Event and record it to our `user_events` table!Since we **never want to destroy our data**, we implemented a boolean `deleted` field on the User model. When a new User is created, it defaults to `false`.
Let’s create a new event, `Events::User::Destroyed`, that will set the `deleted` field to `true`!
## Create an `app/models/events/user/destroy.rb` file
In the same directory as our `Events::User::Created` class, create an equivalent `Events::User::Destroyed` class:
```ruby
# app/models/events/user/destroy.rbclass Events::User::Destroyed < Events::User::BaseEvent
def apply(user)
user
end
end
```
Above, we start with an `apply` method that simply returns the passed-in User instance.To delete a User, we’ll simply require an `id`. Let’s add the `payload_attributes` for it:
```ruby
# app/models/events/user/destroy.rbclass Events::User::Destroyed < Events::User::BaseEvent
payload_attributes :id
end
```And we’ll make our `apply` method update the passed-in User’s `deleted` field to `true`:
```ruby
# app/models/events/user/destroy.rbclass Events::User::Destroyed < Events::User::BaseEvent
payload_attributes :id
def apply(user)
user.deleted = true
user
end
end
```That’s it—our new Event is done!
## Update the `destroy` action in `users_controller`
In our `users_controller`, we’ll make our `destroy` action simply create our new `Events::User::Destroyed` event.Thanks to the `find_or_build_aggregate` and `aggregate_id` methods defined in our top-level BaseEvent, this `”Destroyed”` event will look up a User automatically if a `user_id` argument is supplied.
First, let’s add `id` to the list of strong params in `user_params`:
```ruby
# app/controllers/users_controller.rbprivate def user_params
params.require(:user).permit(:name, :email, :password, :id)
end
```Now, our controller’s `destroy` action can accept a `user_params` that has the necessary `id`. We’ll also use `user_params[:id]` so the event can look up our target User’s record:
```ruby
# app/controllers/users_controller.rbdef destroy
Events::User::Destroyed.create(user_id: user_params[:id], payload: user_params)
end
```We’re ready to go ahead and test with Insomnia!
## Test a DELETE request in Insomnia
Let’s fire up `rails s`.Over in Insomnia, create a new request called `Destroy User` and make it a DELETE:
![Insomnia page showing new Destroy User being set to type DELETE](https://dev-to-uploads.s3.amazonaws.com/i/gpddvf8nf4ccuf5orhyh.png)
Set its target URL to `localhost:3000/users/destroy`:
![Insomnia page showing DELETE request's target URL](https://dev-to-uploads.s3.amazonaws.com/i/j6ffh9x0nmfjivrz87bo.png)
Set the Body type to JSON, and add a hash with a `”user”` key pointing to a hash containing the `”id”`:
![Insomnia page showing DELETE request's JSON body with a user id](https://dev-to-uploads.s3.amazonaws.com/i/w3kq02av0bgb0nvmt3p2.png)
Hit Send, and check the database to see if the Event was created:
![Postico user_events table showing new Destroyed event record, overlaid on Insomnia request](https://dev-to-uploads.s3.amazonaws.com/i/tqiyqf7jdsac9oaau0a7.png)
And finally, let’s check the database to see if our User has `deleted` set to `true`:
![Postico users table showing only User record with deleted field set to true](https://dev-to-uploads.s3.amazonaws.com/i/ciri8xikzflbhjhhvx4m.png)
Perfect! We get to keep our User record, but also have it be `deleted`—we’re having our cake, and eating it too!
![screenshot of cake from video game Portal](https://dev-to-uploads.s3.amazonaws.com/i/bruoty5t9v98as079fut.jpg)
_And that's no lie!_**That’s all it takes to add a new event to our event sourcing system!**
# Conclusion
Wow, we covered a lot of ground! Let’s recap the steps we took to implement our event sourcing system:
- Create a new Rails app, with a User model and controller, and PostgreSQL for the database
- Create an `Events::BaseEvent` class in `app/models/events` to handle Event logic:
- Looking up or creating aggregates (Users)
- Creating getters and setters for `payload_attributes`
- Inferring its own `event_type`
- Hooks for automatically applying changes and saving to the database
- Create a `user_events` table migration
- Create an `Events::User::BaseEvent` to save all Events in its `Events::User::` namespace to the `user_events` table
- Create an `Events::User::Created` event that will apply `user_params` to a new User instance
- Create an `Events::User::Destroyed` event that will look up at User by `id` and set its `deleted` field to `true`This minimal system allows us to do the following:
- Have a record of events that create and destroy Users
- Keep all User data permanently, and still have the ability to scope the `deleted` ones as needed
- A pattern that allows us to easily add new Events that will be saved to the same `user_events` table# Next Up
We have a lot more we can do to improve our event sourcing system, especially around security and data validations! In the next article, we will cover:
- Storing sensitive information safely in Event `payloads`, such as passwords
- Wrapping creating Events in `Commands`, [per Kickstarter’s example]([https://github.com/pcreux/event-sourcing-rails-todo-app-demo/blob/master/app/models/lib/command.rb](https://github.com/pcreux/event-sourcing-rails-todo-app-demo/blob/master/app/models/lib/command.rb))
- Adding validations to `Commands`# References
Special thanks to [Philippe Creux](https://kickstarter.engineering/@pcreux) and [Kickstarter](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224) for sharing [their Event Sourcing example](https://github.com/pcreux/event-sourcing-rails-todo-app-demo).Thanks to [Martin Fowler](https://martinfowler.com/) for his [important writings](https://martinfowler.com/articles/201701-event-driven.html) on [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html).
Thanks to [Arkency](https://arkency.com/) for their great work with [the RailsEventStore library](https://github.com/RailsEventStore/rails_event_store).
And finally, thanks to fellow Dev.to user [Alfredo Motta](https://dev.to/mottalrd) for [sharing about this years ago](https://dev.to/mottalrd/an-introduction-to-event-sourcing-for-rubyists-41e5) (and keeping it up for me to catch up on!).