Philippe Creux β RailsConf 2019
pcreux.com
rails new todozclass Task < ApplicationRecordendclass TasksController < ApplicationController def index # ... end def show # ... end def create # ... endendclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed: true) render json: task end # ...endclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed_at: Time.now) render json: task end # ...end class AddCompletedAtToTasks < ActiveRecord::Migration[6.0] def up add_column 'tasks', :completed_at, :datetime Task.where(completed: true).find_each do |task| task.update!(completed_at: ???) end remove_column 'tasks', :completed end endclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed_at: Time.now, completed_by: current_user) render json: task end # ...end class AddCompletedByToTasks < ActiveRecord::Migration[6.0] def up add_column 'tasks', :completed_by_id, :integer Task.completed.find_each do |task| task.update!(completed_by: ???) end end endclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed_at: Time.now, completed_by: current_user) track_event("Task completed", task_id: task.id, user_id: current_user.id) render json: task end # ...endclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed_at: Time.now, completed_by: current_user) if task.created_by != current_user TaskMailer.queue_task_completed(task: task, recipient: task.created_by) end track_event("Task completed", task_id: task.id, user_id: current_user.id) render json: task end # ...endclass TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed_at: Time.now, completed_by: current_user) if task.created_by != current_user TaskMailer.queue_task_completed(task: task, recipient: task.created_by) end Activity.create!(subject: task, who: current_user, action: :completed) track_event("Task completed", task_id: task.id, user_id: current_user.id) render json: task end # ...endPhilippe Creux
162,000+ creative projects brought to life
thanks to 16,000,000+ backers
who've raised $4,250,000,000+
Philippe Creux β RailsConf 2019
pcreux.com
The application state is the result of a sequence of events
Task Createdtitle: "Prepare sides"β‘ Task #123: "Prepare sides"Task Createdtitle: "Prepare sides"β‘ Task #123: "Prepare sides"Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"β‘ Task #123: "Prepare sides" Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"β‘ Task #123: "Prepare sides" Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"β‘ Task #123: "Prepare slides" Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"β‘ Task #123: "Prepare slides" Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 20th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"Task Due Date Setdate: "2019-04-24"β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"Task Due Date Setdate: "2019-04-24"β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"Task Due Date Setdate: "2019-04-24"Task Completedβ
Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.Task Createdtitle: "Prepare sides"Task Due Date Setdate: "2019-04-20"Task Title Updatedtitle: "Prepare slides"Task Assignedassignee: "Philippe"Task Due Date Setdate: "2019-04-24"Task CompletedNice Properties:
Task Createdtitle: "Prepare sides"event.created_at: "2019-04-15"event.user: "Caroline"event.device: "Desktop"event.ip: "92.43.x.x"Task Due Date Setdate: "2019-04-20"event.created_at: "2019-04-15"event.device: "Desktop"event.ip: "92.43.x.x"...
Task Completedevent.created_at: "2019-04-29"event.user: "Philippe"event.device: "Pixel 3"event.ip: "82.23.x.x"git working directory is the current state commits are events ** travel back and forth, branch out (what if scenarios...?)
business people accounting ledger bank statement: current balance full history
or a data migration that went bad can lead to an inconsistent state
by replaying events you can get back to a sane state
ActivityFeed, Daily Report etc.
class TasksController < ApplicationController def complete event = Events::Task::Completed.create!( task: Task.find(params[:id]), metadata: { user_id: current_user&.id } ) render json: event.task endendclass TasksController < ApplicationController def complete event = Events::Task::Completed.create!( task: Task.find(params[:id]), metadata: { user_id: current_user&.id } ) render json: event.task endend
class Events::Task::Completed < Events::Task::BaseEvent # task is an instance of a Task model (aka "aggregate") def apply(task) task.completed = true task endendclass TasksController < ApplicationController def complete event = Events::Task::Completed.create!( task: Task.find(params[:id]), metadata: { user_id: current_user&.id } ) render json: event.task endend
class Events::Task::Completed < Events::Task::BaseEvent # task is an instance of a Task model (aka "aggregate") def apply(task) task.completed = true task endend
| task_events || ---------------------------------------------------------------- || id | task_id | type | data | metadata | created_at || ---------------------------------------------------------------- || 6 | 10 | Completed | {} | { "user_id": 3 } | 2019-04-01 |class TasksController < ApplicationController def complete event = Events::Task::Completed.create!( task: Task.find(params[:id]), metadata: { user_id: current_user&.id } ) render json: event.task endend
class Events::Task::Completed < Events::Task::BaseEvent def apply(task) task.completed_at = created_at task endend
| task_events || ----------------------------------------------------------------- || id | task_id | type | data | metadata | created_at || ----------------------------------------------------------------- || 6 | 10 | Completed | {} | { "user_id": 3 } | 2019-04-01 |Controller doesn't change.
class Events::Task::Completed < Events::Task::BaseEvent def apply(task) task.completed_at = created_at task endend
class AddCompletedAtToTasks < ActiveRecord::Migration[6.0] def up add_column 'tasks', :completed_at, :datetime # Replay events Task.lock.find_each do |id| task.events.reduce(task) do |task, event| event.apply(task) end task.save! end remove_column 'tasks', :completed end endOn a real / live system, this would be a 5 steps process:
class TasksController < ApplicationController def complete event = Events::Task::Completed.create!( task: Task.find(params[:id]), metadata: { user_id: current_user&.id } ) render json: event.task endend
class Events::Task::Completed < Events::Task::BaseEvent def apply(task) task.completed_at = created_at task.completed_by_id = metadata["user_id"] task endend
| task_events || ----------------------------------------------------------------- || id | task_id | type | data | metadata | created_at || ----------------------------------------------------------------- || 6 | 10 | Completed | {} | { "user_id": 3 } | 2019-04-01 |class Events::Task::Completed < Events::Task::BaseEvent def apply(task) task.completed_at = created_at task.completed_by_id = metadata["user_id"] task endend
class AddCompletedByIdToTasks < ActiveRecord::Migration[6.0] def up add_column 'tasks', :completed_by_id, :integer # Replay events Task.lock.find_each do |id| task.events.reduce(task) do |task, event| event.apply(task) end task.save! end end endUsage:
class Events::Dispatcher < BaseDispatcher # ... on Events::Task::Completed, trigger: Reactors::Task::NotifyCompleted # ...end
module Reactors module Task NotifyCompleted = ->(event) do if event.user != event.task.author TaskMailer.queue_task_completed(task: event.task) end end endendclass Events::Dispatcher < BaseDispatcher # ... on Events::Task::Completed, trigger: Reactors::Task::NotifyCompleted on Events::Task::Completed, trigger: Reactors::ForwardEvent # ...end
module Reactors ForwardEvent = ->(event) do attributes = event.data.merge(event.metadata) EventBus.track_event(event.class.name, attributes) endendclass Events::Dispatcher < BaseDispatcher # ... on Events::Task::Completed, trigger: Reactors::Task::NotifyCompleted on Events::Task::Completed, trigger: Reactors::ForwardEvent on Events::Task::Completed, trigger: Reactors::CreateActivityEntry # ...end
module Reactors CreateActivityEntry = ->(event) do Activity.create!( subject: event.aggregate, user_id: event.metadata["user_id"], recorded_at: event.created_at, action: event.class.name.demodulize.underscore ) endend# InheritanceEvents::BaseEvent <- Events::Task::BaseEvent <- Events::Task::Completed
class Events::Dispatcher < BaseDispatcher # ... on Events::Task::Completed, trigger: Reactors::Task::NotifyCompleted on Events::BaseEvent, trigger: Reactors::ForwardEvent on Events::Task::BaseEvent, trigger: Reactors::CreateActivityEntry # ...endclass Events::Dispatcher < BaseDispatcher # ... on Events::Task::Completed, trigger: Reactors::Task::NotifyCompleted on Events::BaseEvent, async: Reactors::ForwardEvent on Events::Task::BaseEvent, async: Reactors::CreateActivityEntry # ...end


Might want to remove those.
Subscription #123 β Unpaid| Event Type | Data | Created At |
|---|---|---|
| Unpaid | 2019-03-06 22:09:24 | |
| FailedToCollect | invoice_key: 535 | 2019-03-06 22:09:22 |
| FailedToCollect | invoice_key: 535 | 2019-03-05 22:06:15 |
| FailedToCollect | invoice_key: 535 | 2019-03-02 22:01:04 |
| PastDue | 2019-03-02 22:00:48 | |
| InvoiceCreated | invoice_key: 535 | 2019-03-02 21:00:28 |
| Collected | invoice_key: 234 | 2019-02-02 20:52:47 |
| InvoiceCreated | invoice_key: 234 | 2019-02-02 20:52:46 |
| Activated | 2019-02-02 20:52:43 | |
| Created | stripe_key: 123 | 2019-02-02 20:52:35 |
Have a full audit-log such as this one is great for:
Ex: subscription status mismatch




You don't need to make trade offs anymore when designing
currency. Default to USD.


π https://kickstarter.engineering/event-sourcing-made-simple π
Keyboard shortcuts
| β, β, Pg Up, k | Go to previous slide |
| β, β, Pg Dn, Space, j | Go to next slide |
| Home | Go to first slide |
| End | Go to last slide |
| Number + Return | Go to specific slide |
| b / m / f | Toggle blackout / mirrored / fullscreen mode |
| c | Clone slideshow |
| p | Toggle presenter mode |
| t | Restart the presentation timer |
| ?, h | Toggle this help |
| Esc | Back to slideshow |