Philippe Creux β RailsConf 2019
pcreux.com
rails new todoz
class Task < ApplicationRecordend
class TasksController < ApplicationController def index # ... end def show # ... end def create # ... endend
class TasksController < ApplicationController # ... def complete task = Task.find(params[:id]) task.update!(completed: true) render json: task end # ...end
class 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 end
class 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 end
class 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 # ...end
class 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 # ...end
class 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 # ...end
Philippe 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 Created
title: "Prepare sides"
β‘ Task #123: "Prepare sides"
Task Created
title: "Prepare sides"
β‘ Task #123: "Prepare sides"
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
β‘ Task #123: "Prepare sides" Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
β‘ Task #123: "Prepare sides" Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
β‘ Task #123: "Prepare slides" Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
β‘ Task #123: "Prepare slides" Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 20th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
Task Due Date Set
date: "2019-04-24"
β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
Task Due Date Set
date: "2019-04-24"
β‘ Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
Task Due Date Set
date: "2019-04-24"
Task Completed
β
Task #123: "Prepare slides" Assigned to Philippe. Due April 24th, 2019.
Task Created
title: "Prepare sides"
Task Due Date Set
date: "2019-04-20"
Task Title Updated
title: "Prepare slides"
Task Assigned
assignee: "Philippe"
Task Due Date Set
date: "2019-04-24"
Task Completed
Nice Properties:
Task Created
title: "Prepare sides"
event.created_at: "2019-04-15"
event.user: "Caroline"
event.device: "Desktop"
event.ip: "92.43.x.x"
Task Due Date Set
date: "2019-04-20"
event.created_at: "2019-04-15"
event.device: "Desktop"
event.ip: "92.43.x.x"
...
Task Completed
event.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 endend
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 # task is an instance of a Task model (aka "aggregate") def apply(task) task.completed = true task endend
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 # 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 end
On 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 end
Usage:
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 endend
class 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) endend
class 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 # ...end
class 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 |