A full-stack programming model for Ruby and Event Sourcing
Exploring a full stack programming model for Ruby and Event Sourcing, including a time-based approach to domain modelling, durable workflows, time-travelling and reactive server-rendered UIs.
Quantity updated Time "order-123" 1. Order started 2. Product added 3. Quantity updated "order-333" Your order (v4) Latte (2) Croissant (1) Your order (v3) Yoghurt (1)
AddItem do |order, cmd| # Validate business rules raise 'order is not open' unless order.status == 'open' raise 'order can have up to 5 products' if cart.items.size == 5 event ItemAdded, cmd.payload end event ItemAdded do |order, event| order.add_item(**event.payload) end end
AddItem do |order, cmd| # Validate business rules raise 'order is not open' unless order.status == 'open' raise 'order can have up to 5 products' if order.items.size == 5 event ItemAdded, cmd.payload end event ItemAdded do |order, event| order.add_item(**event.payload) end end
AddItem do |order, cmd| # Validate business rules raise 'order is not open' unless order.status == 'open' raise 'order can have up to 5 products' if cart.items.size == 5 event ItemAdded, cmd.payload end event ItemAdded do |order, event| order.add_item(**event.payload) end end
status: nil, items: {} } end event Order::Placed do |listing, event| listing[:status] = 'placed' end # Evolve listing record from events event Order::ItemAdded do |listing, event| listing[:items][event.payload.product_id] = 'open' end # Sync listing record to a file sync do |listing, _, _| File.write("/listings/#{listing[:id]}.json", JSON.dump(listing)) end end
status: nil, items: {} } end event Order::Placed do |listing, event| listing[:status] = 'placed' end # Evolve listing record from events event Order::ItemAdded do |listing, event| listing[:items][event.payload.product_id] = 'open' end # Sync listing record to a file sync do |listing, _, _| File.write("/listings/#{listing[:id]}.json", JSON.dump(listing)) end end
status: nil, items: {} } end event Order::Placed do |listing, event| listing[:status] = 'placed' end # Evolve listing record from events event Order::ItemAdded do |listing, event| listing[:items][event.payload.product_id] = 'open' end # Sync listing record to a file sync do |listing, _, _| File.write("/listings/#{listing[:id]}.json", JSON.dump(listing)) end end
status: nil, items: {} } end event Order::Placed do |listing, event| listing[:status] = 'placed' end event Order::ItemAdded do |listing, event| listing[:items][event.payload.product_id] = 'open' end event Order::ItemFulfillmentStarted do |listing, event| listing[:items][event.payload.product_id] = 'started' end event Order::ItemFulfilled do |listing, event| listing[:items][event.payload.product_id] = ‘fulfilled' end # Sync listing record to a file
event Order::ItemAdded do |listing, event| listing[:items][event.payload.product_id] = 'open' end # Sync listing record to a file sync do |listing, _, _| File.write("/listings/#{listing[:id]}.json", JSON.dump(listing)) end end
:created_at) Start = Sourced::Command.define(...) Started = Sourced::Command.define(...) state do |id| State.new(id, nil, 0, :pending, nil) end command Start do |state, cmd| event :started, cmd.payload end event Started do |state, event| # update local state end # etc end
event| response = PaymentsAPI.pay(event.payload) order_stream = stream_for(event) if response.ok? order_stream.command Order::ConfirmPayment else order_stream.command Order::FailPayment end end end
'pending' } end event Order::OrderFulfilled do |state, event| state[:fulfilled] = true check_deliverable(state) end event Order::PaymentConfirmed do |state, event| state[:paid] = true check_deliverable(state) end event Order::OrderDelivered do |state, event| state[:status] = 'delivered' end
do |state, event| return unless state[:status] == 'ready' stream_for(event).command(Order::DeliverOrder) do |cmd| cmd.delay Time.now + 5 # Simulate a delay for delivery end end private def check_deliverable(state) state[:status] = 'ready' if state[:fulfilled] && state[:paid] end