Upgrade to Pro — share decks privately, control downloads, hide ads and more …

15 years with Rails and DDD (AI Edition)

15 years with Rails and DDD (AI Edition)

This talk presents what is the current problem with Rails apps from AI agents perspective. The lack of modularity in Rails apps make agents confused - the context is too big for them. The solution is making a big number of small modules with well defined interfaces.

Domain Driven Design (DDD) is a technique which, together with event-driven and CQRS results in smaller modules.

The app I am presenting has 50 small modules, composed via commands and events.

I am also presenting an algorithm which helps reducing the size of ActiveRecord models - by gradually moving their responsibilities to read models and aggregates.

Look at this repo to see the modularity mentioned - https://github.com/RailsEventStore/ecommerce

Avatar for Andrzej Krzywda

Andrzej Krzywda

January 31, 2026
Tweet

More Decks by Andrzej Krzywda

Other Decks in Technology

Transcript

  1. AI

  2. Fallout-themed rpg roguelike game • dragonruby (mruby) • voice-driven dev

    • perks, challenges, SPECIAL, levels • built with my 11 yrs son
  3. Usually Rails teams hit the wall after 2-5 years of

    CRUDs now it can be 2-5 days of vibe coding at ~100 ActiveRecords complexity
  4. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 Modern DDD building blocks commands + events
  5. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20
  6. create_table "customers", id: :uuid, force: :cascade do |t| t.string "name"

    t.boolean "vip", t.decimal "paid_orders_summary", t.uuid "account_id" t.uuid "store_id" end
  7. module Customers class Customer < ApplicationRecord self.table_name = "customers" end

    private_constant :Customer def self.customers_for_store(store_id) Customer.where(store_id: store_id) end def self.find_customer_in_store(customer_id, store_id) Customer.where(store_id: store_id).find(customer_id) end class Configuration def call(event_store) event_store.subscribe(RegisterCustomer.new, to: [Crm::CustomerRegistered]) event_store.subscribe(AssignStoreToCustomer.new, to: [Stores::CustomerRegistered] event_store.subscribe(PromoteToVip.new, to: [Crm::CustomerPromotedToVip]) event_store.subscribe(UpdatePaidOrdersSummary.new, to: [Fulfillment::OrderConfirm event_store.subscribe(ConnectAccount.new, to: [Authentication::AccountConnectedTo end end end 80 LOC Read model
  8. module Customers class Configuration def call(event_store) event_store.subscribe(RegisterCustomer.new, to: [Crm::CustomerRegistered]) event_store.subscribe(AssignStoreToCustomer.new,

    to: [Stores::CustomerRegistered]) event_store.subscribe(PromoteToVip.new, to: [Crm::CustomerPromotedToVip]) event_store.subscribe(UpdatePaidOrdersSummary.new, to: [Fulfillment::OrderConfirmed]) end end end Read model
  9. module Customers class PromoteToVip def call(event) promote_to_vip(event) end private def

    promote_to_vip(event) find(event.data.fetch(:customer_id)).update(vip: true) end def find(customer_id) Customer.where(id: customer_id).first end end end Read model
  10. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20
  11. 220 LOC Domain module Inventory class Supply < Infra::Command attribute

    :product_id, Infra::Types::UUID attribute :quantity, Infra::Types::Coercible::Integer.constrained(gteq: 1) end end module Inventory class StockLevelChanged < Infra::Event end end def supply(quantity) apply StockLevelChanged.new( data: { product_id: @product_id, quantity: quantity, stock_level: (@in_stock || 0) + quantity } ) apply_availability_changed end
  12. Domain module Inventory class StockLevelChanged < Infra::Event end end def

    supply(quantity) apply StockLevelChanged.new( data: { product_id: @product_id, quantity: quantity, stock_level: (@in_stock || 0) + quantity } ) apply_availability_changed end
  13. module Inventory class InventoryEntry include AggregateRoot InventoryNotAvailable = Class.new(StandardError) InventoryNotEvenReserved

    = Class.new(StandardError) def initialize(product_id) @product_id = product_id @reserved = 0 end def supply(quantity) apply StockLevelChanged.new( data: { product_id: @product_id, quantity: quantity, stock_level: (@in_stock || 0) + quantity } ) apply_availability_changed end Domain def dispatch(quantity) end def reserve(quantity) end def release(quantity) end private def apply_availability_changed apply AvailabilityChanged.new( data: { product_id: @product_id, available: availability } ) if stock_level_defined? end on StockLevelChanged do |event| @in_stock = event.data.fetch(:stock_level) end
  14. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20
  15. module Processes class ShipmentProcess include Infra::ProcessManager.with_state { ProcessState } subscribes_to(

    Shipping::ShippingAddressAddedToShipment, Fulfillment::OrderRegistered, Fulfillment::OrderConfirmed, Stores::OfferRegistered ) private def act case state in { shipment: :address_set, order: :placed } register_shipment submit_shipment in { shipment: :address_set, order: :confirmed } register_shipment submit_shipment authorize_shipment else end end 67 LOC def apply(event) case event when Shipping::ShippingAddressAddedToShipment state.with(shipment: :address_set) when Fulfillment::OrderRegistered state.with(order: :placed) when Fulfillment::OrderConfirmed state.with(order: :confirmed) when Stores::OfferRegistered state.with(store_id: event.data.fetch(:store_id)) end end def register_shipment return unless state.store_id command_bus.call( Stores::RegisterShipment.new( shipment_id: id, store_id: state.store_id ) ) end def submit_shipment command_bus.call(Shipping::SubmitShipment.new(order_id: id)) end def authorize_shipment command_bus.call(Shipping::AuthorizeShipment.new(order_id: id)) end def fetch_id(event) event.data.fetch(:order_id) end ProcessState = Data.define(:order, :shipment, :store_id) do def initialize(order: nil, shipment: nil, store_id: nil) = super end end end Process Managers
  16. module Processes class ShipmentProcess include Infra::ProcessManager.with_state { ProcessState } subscribes_to(

    Shipping::ShippingAddressAddedToShipment, Fulfillment::OrderRegistered, Fulfillment::OrderConfirmed, Stores::OfferRegistered )
  17. module Processes class ShipmentProcess include Infra::ProcessManager.with_state { ProcessState } subscribes_to(

    Shipping::ShippingAddressAddedToShipment, Fulfillment::OrderRegistered, Fulfillment::OrderConfirmed, Stores::OfferRegistered )
  18. module Processes class ShipmentProcess include Infra::ProcessManager.with_state { ProcessState } def

    act case state in { shipment: :address_set, order: :placed } register_shipment submit_shipment in { shipment: :address_set, order: :confirmed } register_shipment submit_shipment authorize_shipment else end end
  19. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 Controller
  20. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 class CustomersController < ApplicationController def index @customers = Customers.customers_for_store(current_store_id) end def create ActiveRecord::Base.transaction do create_customer(params[:customer_id], params[:name]) end rescue Crm::Customer::AlreadyRegistered flash[:notice] = "Customer was already registered" render "new" else redirect_to customers_path end def create_customer(customer_id, name) command_bus.(create_customer_cmd(customer_id, name)) command_bus.(register_customer_in_store_cmd(customer_id)) end def create_customer_cmd(customer_id, name) Crm::RegisterCustomer.new( customer_id: customer_id, name: name) end
  21. class CustomersController < ApplicationController def index @customers = Customers.customers_for_store(current_store_id) end

    def create ActiveRecord::Base.transaction do create_customer(params[:customer_id], params[:name]) end rescue Crm::Customer::AlreadyRegistered flash[:notice] = "Customer was already registered" render "new" else redirect_to customers_path end def create_customer(customer_id, name) command_bus.(create_customer_cmd(customer_id, name)) command_bus.(register_customer_in_store_cmd(customer_id)) end def create_customer_cmd(customer_id, name) Crm::RegisterCustomer.new( customer_id: customer_id, name: name) end
  22. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 def create_customer_cmd(customer_id, name) Crm::RegisterCustomer.new( customer_id: customer_id, name: name) end
  23. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 def create_customer_cmd(customer_id, name) Crm::RegisterCustomer.new( customer_id: customer_id, name: name) end
  24. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 module Crm class CustomerRegistered attribute :customer_id attribute :name, end end
  25. Inventory Pricing CRM Catalog Shipments Taxes Communication x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 SendWelcomeMessage module Crm class CustomerRegistered attribute :customer_id attribute :name, end end
  26. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 def index @customers = Customers.customers_for_store end
  27. Inventory Pricing CRM Catalog Shipments Taxes Invoicing x14 Domains Orders

    Customers Products ClientOrders ClientInbox PublicO ff er Read models x16 Reservation Shipment InvoiceGeneration Onboarding Process Managers x20 class CustomersController < ApplicationController def index @customers = Customers.customers_for_store end def create ActiveRecord::Base.transaction do create_customer(params[:customer_id], params[:name]) end rescue Crm::Customer::AlreadyRegistered flash[:notice] = "Customer was already registered" render "new" else redirect_to customers_path end