From ActiveRecord to EventSourcing

7,301 views
6,872 views

Published on

An introduction to a possible implementation of CQRS/ES architecture for a Ruby on Rails app. It starts from Domain Model to arrive to a sample app that implements the Event Sourcing pattern. This presentation was part of Wroclove_rb 2014 conference in Wraclow (PL)

Published in: Technology

From ActiveRecord to EventSourcing

  1. 1. From ActiveRecord to Events Emanuele DelBono @emadb
  2. 2. Customer Address Invoice Items Contacts Role Contract Price City
  3. 3. @emadb I’m a software developer based in Italy. I develop my apps in C#, Javascript and some Ruby. I’m a wannabe Ruby dev.
  4. 4. Lasagna architecture
  5. 5. View Controller Model (AR) Database
  6. 6. O/RM
  7. 7. Active record “An object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.” M. Fowler
  8. 8. Active record 1 table => 1 class Too coupled with the database structure No SRP Database first class User < ActiveRecord::Base attr_accessible :email, :password belongs_to :group end
  9. 9. Get vs Post def index @products = Product.all end ! def create @product = Product.new(product_params) if @product.save redirect_to @product else render action: 'new' end end
  10. 10. Active Record A single model cannot be appropriate for reporting, searching and transactional behaviour. Greg Young
  11. 11. Read vs Write Reads and writes are two different concerns Reads are simpler Writes need logic
  12. 12. KEEP CALM AND STOP THINKING IN CRUD
  13. 13. Command Query Responsibility Segregation
  14. 14. CQRS Presentation Layer Handler BL Repository Write DB Read DB Query Service Denormalizer
  15. 15. CQRS Fully normalized Reads are easy Writes became easier SELECT fields FROM table (WHERE …)
  16. 16. Object state id basket_id article_id quantity 1 4 8 1 2 3 8 3 3 3 6 1 4 4 5 1
  17. 17. Thinking in events Every change is an event. add_item 1 add_item 2 remove_item 1 add_item 3 time
  18. 18. Event Sourcing Capture all changes to an application state as a sequence of events. M.Fowler If the changes are stored in a database, we can rebuild the state re-applying the events.
  19. 19. Event Sourcing Presentation Layer Bus Handler DMRepository Event store Denormalizer Query service Read DB Command Events
  20. 20. Pros • Encapsulation • Separation of concern • Simple storage • Performance/Scaling • Simple testing • More information granularity • Easy integration with other services
  21. 21. Cons • Complex for simple scenarios • Cost of infrastructure • Long-living objects needs time to be reconstructed • Tools needed (i.e. rebuild the state)
  22. 22. http://antwonlee.com/
  23. 23. Ingredients • Rails app (no ActiveRecord) • Redis (pub-sub) • Sequel for querying data • MongoDb (event store) • Sqlite (Read db) • Wisper (domain events)
  24. 24. Domain Model Basket BasketItem Article * 1 • Fully encapsulated (no accessors) • Fully OOP • Events for communication • PORO
  25. 25. show_me_the_code.rb
  26. 26. include CommandExecutor ! def add_to_basket send_command AddToBasketCommand.new( {"basket_id" => 42, "article_id" => params[:id].to_i}) redirect_to products_url end Controller Bus Command POST /Products add_to_basket
  27. 27. module CommandExecutor ! def send_command (command) class_name = command.class.name channel = class_name.sub(/Command/, '') @redis.publish channel, command.to_json end ! end send_command
  28. 28. def consume(data) basket = repository.get_basket(data["basket_id"]) article = repository.get_article(data["article_id"]) basket.add_item article ! basket.commit end Bus Handler Command handler
  29. 29. class Basket include AggregateRootHelper ! def add_item (item) raise_event :item_added, { basket_id: id, item_code: item.code, item_price: item.price } end # ... ! end add_item
  30. 30. def raise_event(event, args) @uncommited_events << {name: event, args: args} send "on_#{event}", args end raise_event DM (Basket) Events
  31. 31. def get_item (item_code) @items.select{|i| i.item_code == item_code}.try :first end ! def on_item_added (item) get_item(item[:item_code]).try(:increase_quantity) || @items << BasketItem.new(item) end on_item_added DM (Basket) Events
  32. 32. def commit while event = uncommited_events.shift events_repository.store(id, event) send_event event end end commit DM (Basket) Event store
  33. 33. Event Store
  34. 34. def item_added(data) db = Sequel.sqlite(AppSettings.sql_connection) article = db[:products_view].where(code: data[:item_code]).first basket = db[:basket_view].where('basket_id = ? AND article_id = ?', data[:basket_id], article[:id].to_i).first if basket.nil? #insert else db[:basket_view].where(id: basket[:id]).update(quantity: (basket[:quantity] + 1)) end end denormalizer Denormalizer
  35. 35. Read-Db
  36. 36. def index @products=db[:basket_view] end Controller Read DB Query GET /Products index
  37. 37. Conclusion • Stop thinking in CRUD • Read and Write are different • Domain model should be based on PORO • CQRS/ES is useful in complex scenario • Ruby power helps a lot (less infrastructure code)
  38. 38. https://github.com/emadb/revents

×