DDD course notes for sales challenge
by kkp
Some articles to read before/after this:
- https://railseventstore.org/docs/v1/app/
Design Challenge
Given a certain business description, provide a design in form of code, drawing, tests or whatever.
Example Design challenge to implement:
Sales Processing:
Actors
- Sales person
- Company owning products
- Commission platform
Requirements
- Sales person sells a product. They get a commission based on agreed conditions, from the company. The commission platform receives an agreed (with the company) N% from each commission too.
- Sales people can sell products from different companies
- The company can see, each month, how much they need to pay to each sales person and why
- The platform admins can see how much they will charge the companies
- The sales person knows at any moment how much of their sales they made - how much commission was made
What ifs
- we get a sale information, but the product was not provided yet?
- we get a sale info, but we don’t have the commission agreement with the sales man?
- what if the commission agreement can change, also back in time?
My design of design challenge:
Base Files structure for just sale made looks like that:

And the base domain design for viewing at this link
Here is my explanation for this structure step by step:
Objects
- Sales Person: main player here that makes the sales, and puts most thing into motion. His presence is rather obvious, he has to be registered with every sale as the one who made it.
- Company: when a sale is made, product that was sold is allocated to certain company. That Company has an agreement with a sales person that we need.
- Platform: A passive object that takes % from each commission of certain company that it is associated with.
- Product: Registered on company, needs to have a price field, and can come in quantities. Can be depleted from stock.
- Sale: Although it sounds more like an event, it also needs to be saved in database, registered or something similiar, since it holds a lot of important information, and its state can change a lot (cancel, confirm, return etc).
- Agreement: Signed between sales person and a company, contains a percentage and a status (:draft, :rejected, :accepted)
Events
- SaleMade main thing to happen in this event are
    - Product is taken out of stock in quantity given - separate command needs to be issued with its handler
- Commissions get distributed (to platform and sales person) (they are calculated too before that maybe, the last event can happen on its own, but should it happen here too? To be certain that commissions are up to date?)
 
- AgreementCreated - When a sales persons signs in with a company, they have to agree on certain %. When they do it for the 1st time this event gets called to save agreement details into DB.
- AgreementEdited - Agreement details can change. This accounts for that. This can retroactively change the past commissions received from sales made on old terms, making up for the third what if
- ProductProvided - Just a stock replenish event, but can also help with the first what if
- CommissionsApplied - This event will cause commissions for both the sales person and the platform to get calculated. Fired after sale is made, or on demand when altering agreements
This is written as I go so some questions are still here, along with uncertainties. On to the code. This app’s code is available at GH this time it is fully flesh out mini app that works (with some mocks and assumptions, like 1 product for 1 sale but in many quantities, no discounts, simple relationships, but still).
Note
This uses nice helpers by Arkency as defined here I use almost the same (minus the types that I will not use, slight modification in Event) in my lib/ since they are nice and simple and obviously made to work with RES. I recommend checking the source code of them it is some clever stuff! I will skip showing snippets of migrations, application.rb config etc. cause that is not what is important in this lesson. This note is mostly about contracting the idea and design. I have included basic seeds to make the base example work.
#command
module Sales
  class MakeSale < Command
    attribute :sale_id, Types::ID
    attribute :product_id, Types::ID
    attribute :quantity, Types::Coercible::Integer
    alias :aggregate_id :sale_id
  end
end
#command handler
module Sales
  class OnSaleMake
    include CommandHandler
    def call(command)
      with_aggregate(SaleRoot, command.aggregate_id) do |sale|
        sale.make_sale(command.sale_id, command.quantity, command.product_id)
      end
    end
  end
end
#event
module Sales
  class SaleMade < Event
    attribute :sale_id, Types::ID
    attribute :product_id, Types::ID
    attribute :quantity, Types::Integer
  end
end
#Root
require 'aggregate_root'
  require 'aggregate_root'
module Sales
  class SaleRoot
    include AggregateRoot
    NotInStock = Class.new(StandardError)
    NoAgreementWithCompany = Class.new(StandardError)
    def initialize(id)
      @id = id
      @state = "in_progress"
    end
    def make_sale(sale_id, quantity, product_id)
      raise NotInStock if product_quantity_not_enough?(product_id,quantity)
      raise NoAgreementWithCompany if no_agreement?(product_id, sale_id)
      apply SaleMade.new(data: { sale_id: sale_id, quantity: quantity, product_id: product_id })
    end
    on SaleMade do |event|
      @state = "sold"
    end
    private
    def no_agreement?(product_id, sale_id)
      # bad i know, thats not the point here though
      Agreement.find_by(
        owner: SalesPerson.find(Sales::Sale.find(sale_id)),
        company_id: Products::Product.find(product_id).company.id
      )
    end
    def product_quantity_not_enough?(product_id, quantity)
      Products::Product.find(product_id).quantity - quantity < 0
    end
  end
end
#readmodels
module Sales
  class OnSaleMadeEvent
    def call(event)
      find_sale(event.data[:sale_id])
      find_product(event.data[:product_id])
      @sale.update(total: event.data[:quantity] * @product.price)
    end
    private
    def find_product(id)
      @product ||= Products::Product.find(id)
    end
    def find_sale(id)
      @sale ||= Sales::Sale.find(id)
    end
  end
end
module Products
  class OnSaleMadeEvent
    def call(event)
      product = Products::Product.find(event.data[:product_id])
      product.update(quantity: product.quantity - event.data[:quantity])
    end
  end
end
#config
# Subscribe event handlers below
  Rails.configuration.event_store.tap do |store|
    #I added Event part cause it was too similair to command handler (made -> make) at this stage I still need it to make it easier to different classes
    store.subscribe(Sales::OnSaleMadeEvent, to: [Sales::SaleMade])
    store.subscribe(Products::OnSaleMadeEvent, to: [Sales::SaleMade])
    #Note that two read models subscribe to the same event.s
    store.subscribe_to_all_events(->(event) { Rails.logger.info(event.event_type) })
  end
  Rails.configuration.command_bus.tap do |bus|
    # ADD COMMAND HANDLERs TO THE COMMANDs
    bus.register(Sales::MakeSale, Sales::OnSaleMake.new)
  end
end
This is what the first version looks like, just a base scenario of making a sale by itself.
irb(main):004:0> Products::Product.find(7).quantity
  Products::Product Load (0.6ms)  SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
=> 11
We have some Product7 in stock, lets go and register a sale

> Products::Product.find(7).quantity
Products::Product Load (0.6ms)  SELECT "products".* FROM "products" WHERE "products"."id" = ? LIMIT ?  [["id", 7], ["LIMIT", 1]]
=> 4
> Sales::Sale.last.total.to_f
Sales::Sale Load (0.6ms)  SELECT "sales".* FROM "sales" ORDER BY "sales"."id" DESC LIMIT ?  [["LIMIT", 1]]
=> 49.0
Stuff that we wanted to happened did happen. Awesome. To check up on the events
ap RailsEventStoreActiveRecord::Event.last
  RailsEventStoreActiveRecord::Event Load (0.3ms)  SELECT "event_store_events".* FROM "event_store_events" ORDER BY "event_store_events"."id" DESC LIMIT ?  [["LIMIT", 1]]
#<RailsEventStoreActiveRecord::Event:0x000055f99a8d6118> {
            :id => "e880e3bf-0df0-4cbc-bf98-f89dc217ec22",
    :event_type => "Sales::SaleMade",
      :metadata => "---\n:timestamp: 2020-12-09 12:01:24.355745249 Z\n:remote_ip: \"::1\"\n:request_id: c2a10488-3abc-4b50-a540-ad7630b2c040\n",
          :data => "---\n:sale_id: 2\n:product_id: 7\n:quantity: 1\n",
    :created_at => Wed, 09 Dec 2020 12:01:24 UTC +00:00
}
We have event data saved. Now we need to deal with commissions logic, and sales person getting correct commission from the company that owns the product he sold. We will also account for the edge cases (what ifs)
Saga Note
I think this process could also work as a saga, with read models for counting total money made from sale, subtracting products from inventory and calculating commissions. But for the sake of getting a better grasp on what has been mostly covered so far I continued making it that way.
I decided to create a new Bounded Context for commissions:
- They need to react to certain events, like change of agreement, sale made
- Those events come from different BC’s. Retracing back to older notes that I made, this point makes a lot more sense now
    Communication between BC happens through Domain Events, so when something happens in the code of BC, that can concern other BC, an event is being published. Other contexts receive this event, and does something with it.
So commissions code looks somewhat like that (below). Im skipping here displaying the commissions since that rather straightforward. All that we need to do is collect correct events. In case of agreement change that modifies the commissions in past sales that is also super easy thanks to event sourcing. When we change agreements, we fire up calculation for all sales of given sales person or platform depending on who changes the agreement. When we got those new calculations, the display would just need to take events for each sale that are the newest (specific events about calculating the commissions). Then display the correct value field from the event. I did not write that code since its rather easy and straightforward and the code below for base commissions already help me a lot in grasping the whole idea and design of the rest is enough for me here. Pretty happy with how it all is coming together.

#read models
module Commissions
   class OnCommissionsApplied
      def call(event)
         # here we could continue with notyfing about commissionsn etc. but in general now displayt matters more but
         # that is outside the scope of this example app
      end
   end
end
# frozen_string_literal: true
module Commissions
   class OnSaleMadeEvent
      def call(event)
         Rails.configuration.command_bus.(Commissions::DistributeSaleCommissions.new(sale_id: event.data[:sale_id]))
      end
   end
end
#AR
require 'aggregate_root'
module Commissions
   class CommissionRoot
      include AggregateRoot
      def initialize(id)
         @id = id
      end
      def distribute_commissions(sale_id)
         total = calculate_sale_total(sale_id)
         apply CommissionsApplied.new(
                 data: {
                         sale_id: sale_id,
                         commission_sales_person: calculate_commission(sales_person_agreement.percentage, total),
                         commission_platform: calculate_commission(platform_agreement.percentage, total)
                 }
         )
      end
      on CommissionsApplied do |event|
         puts "handle"
      end
      private
      def calculate_commission(percentage, total)
         total.to_f * percentage / 100.to_f
      end
      def sales_person_agreement
         Agreement.find_by(owner: @sale.sales_person, company: company)
      end
      def platform_agreement
         Agreement.find_by(owner: platform, company: company)
      end
      def company
         @company ||= Company.find(@sale.company_id)
      end
      def platform
         #doing more for platform in terms of model and architecture seemed like an overkill for this lesson
         @platform ||= company.platform
      end
      def calculate_sale_total(sale_id)
         # Not getting total from model in case of race issue between events. This is why saga might have been a good idea.
         # A different solution is passing more data (not sale ID but sale info, so price of product and quantity, to avoid
         # the query). But this is the simplest in terms of implementation and fastest, the least code, and this challenge
         # is mostly about design and getting a better grip on the ideas themselves.
         sale(sale_id).quantity * Products::Product.find(@sale.product_id).price.to_f
      end
      def sale(sale_id)
         @sale ||= Sales::Sale.find(sale_id)
      end
   end
end
#event
module Commissions
   class CommissionsApplied < Event
      attribute :sale_id, Types::ID
      attribute :commission_sales_person, Types::Float
      attribute :commission_platform, Types::Float
   end
end
#command
module Commissions
   class DistributeSaleCommissions < Command
      attribute :sale_id, Types::ID
      alias :aggregate_id :sale_id
   end
end
#CH
module Commissions
   class OnDistributeSaleCommissions
      include CommandHandler
      def call(command)
         with_aggregate(CommissionRoot, command.aggregate_id) do |commission|
            commission.distribute_commissions(command.sale_id)
         end
      end
   end
end
#updated config
require 'rails_event_store'
require 'aggregate_root'
require 'arkency/command_bus'
Rails.configuration.to_prepare do
   Rails.configuration.event_store = RailsEventStore::Client.new
   Rails.configuration.command_bus = Arkency::CommandBus.new
   AggregateRoot.configure do |config|
      config.default_event_store = Rails.configuration.event_store
   end
   # Subscribe event handlers below
   Rails.configuration.event_store.tap do |store|
      # store.subscribe(Movies::OnMovieAddedToRepertoire, to: [Movies::MovieAddedToRepertoire]) #READ MODEL SUBSCRIBES TO EVENT
      #I added Event part cause it was too similair to command handler (made -> make) at this stage I still need it to make it easier to different classes
      store.subscribe(Sales::OnSaleMadeEvent, to: [Sales::SaleMade])
      store.subscribe(Products::OnSaleMadeEvent, to: [Sales::SaleMade])
      store.subscribe(Commissions::OnSaleMadeEvent, to: [Sales::SaleMade])
      store.subscribe(Commissions::OnCommissionsApplied, to: [Commissions::CommissionsApplied])
      #Note that more than 1 read models subscribe to the same event
      store.subscribe_to_all_events(->(event) { Rails.logger.info(event.event_type) })
   end
   Rails.configuration.command_bus.tap do |bus|
      # ADD COMMAND HANDLERs TO THE COMMANDs
      # bus.register(Movies::AddMovieToRepertoire, Movies::OnMovieAddToRepertoire.new(imdb_adapter: OpenStruct.new(fetch_number: "Mocked")))
      bus.register(Sales::MakeSale, Sales::OnSaleMake.new)
      bus.register(Commissions::DistributeSaleCommissions, Commissions::OnDistributeSaleCommissions.new)
   end
end
I have simplified errors to the cases where there is no agreement or products in stock. There are obviously other, better more complex solutions to it, but writing all this in code here would be overkill for a design challenge. So a better solution would be something like:
- Allow register a sale in one of the problematic cases, do not calculate commissions, and use UI to display info about problems
- Display the UI info both during sale process and in the panels for sales person and company to let them know, there is a registered sale without agreement between them. Events are ok for it.
- Process Manager could also come in handy in this scenarios. Maybe send out agreements to send and request to restock. Or maybe schedule PM to keep tracks of agreements and products in stock, and when everythiong checks out, it processes the commissions flow. I like this one better it requires a lot more code, cause PM needs to monitor other events and then issue other commands, but its more on the READ side instead of write side, which already does a lot of work in this process An interesting idea would not to keep commissions calculation on the write side, after sale is made, just read it everytime it is being accessed.