DDD course notes for backend architecture!
by kkp
BackendArchitectureNotes
Some articles to read before/after this:
- https://martinfowler.com/articles/201701-event-driven.html - nice read about the topic from Martin Fowler
- https://blog.arkency.com/2016/09/command-bus-in-a-rails-application/ - about command bus gem, but also as a pattern of commands in CQRS overall
- https://blog.arkency.com/process-managers-revisited/ - about Process Managers pattern
- https://blog.arkency.com/2017/06/dogfooding-process-manager/ - also about Process Managers pattern
    CQRS READ MODELS
In CQS a method must either be a command that performs an action or a query that returns data. Never both
Most apps are 90% read and 10% write. So separating read from write can have a huge performance boost. Also separating write is rather important, since it protects, your domain and business logic better.
Pros of CQRS in short:
- Improve read operations performance
    - Data store of read models is optimized for reads
- Scales independently
 
- Simplicity and ease of changes in read models
    - thor away and rebuild
 
- Decluttering the domain
    - not exposing public attrs for reads
- easier to test, since you can only test the write side Cons are obviously a requirement of changing the mindset. It is a big infrastructure too, larger codebase. Write operations might rake longer, but do no need to.
 
Read Model
A good read model will be/have:
- Read Optimized
    - Denormalized data (duplicating is ok, when it helps you to quickly display data).
- Tailor Made - do not force using the same read model for every place/page/view etc. Prepare exactly what you need, it makes read models simpler.
 
- Re-Creatable
    - it is build completely from domain events.
- you can throw it away and rebuild it historic from domain data, when you achieve the above point
 
RM can be build without Domain Event ofc. but its not decoupled, it cannot be easily deactivated/refactored.
Read Models and caching:
- RM is the solo consumer of changing events, we always know when data will change
- Caching is time based or key-attribute based
CQRS does not have to be a top level architecture. It can be applied as part of the system, whenever the benefits outweigh the costs.
Read Models should limit the logic as much as possible, its best if you manage to keep them as places where you ‘dump’ data.
Solution to it, is trying to handle as much logic as possible on the write side ofc. Write side should be more complicated.
Event Sourcing
Storing all changes in form of domain events
Pros:
- no OR mapping, means avoidance of impedance mismatch
- append only store, not losing any data, like ever
- audit logs come with
- rebuilding state is easier so, so much
- replay events to reprocess old data
- temporal queries are easy
- different read models possible
- easy to scale, start small go big with the same architecture
Cons:
- no rails c and change some data on prod
- no data migration
- huge mindset change
- need separate models for reporting
- lack of tooling
How it even works:
- Read old models from DB
- Apply historical events to change state
- Call a method/command
    - Verify variants
- Apply new events to change state
 
- Save new events to database and publish to MQ (message que)
Only safe way to change is to apply new events.
Applying the event cannot have side effects outside the object in question.
Remember that bringing too much data into events is an anti pattern, and means that there is not enough decoupling in your system. You probably need more divided events etc. in those situations.
Process Managers and Sagas
Example system
- PostalAddedToOrder - #event
- PostalAddressFilledOut - #event
- PdfGenerated - #event
- 
    PaymentPaid - #event 
- => SendPdfViaPostal #command
Events do not need to happen in that exact order though.
Saga
A saga is a “long-lived business transaction or process”. The problem with aggregates is that they only care about their little part of the universe. Sagas, on the other hand, are coordinating objects. They listen for what’s happening and they tell other objects to take appropriate action. Sagas manage process. They contain business behavior, but only in the form of process. This is a critical point. Sagas, in their purest form, don’t contain business logic.
Sagas work on commands mostly, since command are simpler concepts, they produce a clear result.
- Domain events work in publish-subscribe way. When an event happens, there can be multiple subscribers. In commands there is 1-to-1 mapping. 1 command 1 command handler
Both commands and events are messages basically, that produce the output based on input. But Events operate in past tense, while commands represent an intent. Saga can take many events as an input, as presented in the example system. All those events can be presented as an input to the Saga that will then invoke the correct command. Saga can for example inherit from ApplicationJob, use sidekiq etc. Saga can also for example handle cases like the same event happening several times. For example if you have an event about order being purchased, and you get a discount after 5 purchased orders, Saga will await receiving 5 of those event and then send a command that will generate discount.
Process Managers
Also a pattern that can be helpful in modeling long running business processes. They usually consist of multiple steps, they do not have to happen in any specific order etc. or one after another. This pattern is helpful when there can be some kind of ‘race’ problems, when events can happen in different order everytime they happen, when the whole process in initialized. Like in sagas, PM operates on multiple domain events and produces a command. It is kind of a big event handler, listening to many events at once.
- Process Manager is at its basis a function. It takes some events as an argument and then issues a command. Command triggers processes in the rest of the system.
- Its important to distinct PM from aggregate. PM automates a certain process, orchestrating flow between several bounded contexts (usually)
- PM has a state, that can be done using AR, Event Sourcing state or whatever you want.
- PM fetches the current state, applies new domain event based on the state and decides if the system needs to perform some action if yes it invokes a correct command to issue that.
Example process manager, for processing the payment, using standard credit card flow with authorize, capture and release.
class PaymentProcess
  def initialize(store: Rails.configuration.event_store,
                 bus: Rails.configuration.command_bus)
    @store = store
    @bus = bus
  end
  def call(event)
    state = build_state(event)
    if state.release?
      bus.call(Payments::ReleasePayment.new(
        order_id: state.order_id,
        transaction_id: state.transaction_id))
    end
  end
  private
  attr_reader :store, :bus
  def build_state(event)
    stream_name = "PaymentProcess$#{event.data.fetch(:order_id)}"
    past = store.read.stream(stream_name).to_a
    last_stored = past.size - 1
    store.link(event.event_id, stream_name: stream_name, expected_version: last_stored)
    ProcessState.new.tap do |state|
      past.each{|ev| state.call(ev)}
      state.call(event)
    end
  rescue RubyEventStore::WrongExpectedEventVersion
    retry
  end
  class ProcessState
    def initialize
      @order = :draft
      @payment = :none
    end
    attr_reader :transaction_id, :order_id
    def call(event)
      case event
      when Payments::PaymentAuthorized
        @payment = :authorized
        @transaction_id = event.data.fetch(:transaction_id)
      when Payments::PaymentReleased
        @payment = :released
      when Ordering::OrderSubmitted
        @order = :submitted
        @order_id = event.data.fetch(:order_id)
      when Ordering::OrderExpired
        @order = :expired
      when Ordering::OrderPaid
        @order = :paid
      end
    end
    def release?
      @payment == :authorized && @order == :expired
    end
  end
end
And a test for it
require 'test_helper'
class PaymentProcessTest < ActiveSupport::TestCase
  test 'happy path' do
    fake = FakeCommandBus.new
    process = PaymentProcess.new(bus: fake)
    given([
      order_submitted,
      payment_authorized,
      order_paid
    ]).each do |event|
      process.call(event)
    end
    assert_nil(fake.received)
  end
  test 'order expired without payment' do
    fake = FakeCommandBus.new
    process = PaymentProcess.new(bus: fake)
    given([
      order_submitted,
      order_expired,
    ]).each do |event|
      process.call(event)
    end
    assert_nil(fake.received)
  end
  test 'order expired after payment authorization' do
    fake = FakeCommandBus.new
    process = PaymentProcess.new(bus: fake)
    given([
      order_submitted,
      payment_authorized,
      order_expired,
    ]).each do |event|
      process.call(event)
    end
    assert_equal(fake.received,
      Payments::ReleasePayment.new(transaction_id: transaction_id)
    )
  end
  test 'order expired after payment released' do
    fake = FakeCommandBus.new
    process = PaymentProcess.new(bus: fake)
    given([
      order_submitted,
      payment_authorized,
      payment_released,
      order_expired,
    ]).each do |event|
      process.call(event)
    end
    assert_nil(fake.received)
  end
  private
  class FakeCommandBus
    attr_reader :received
    def call(command)
      @received = command
    end
  end
  def transaction_id
    @transaction_id ||= SecureRandom.hex(16)
  end
  def order_id
    @order_id ||= SecureRandom.uuid
  end
  def order_number
    '2018/12/16'
  end
  def customer_id
    123
  end
  def given(events, store: Rails.configuration.event_store)
    events.each{|ev| store.append(ev)}
    events
  end
  def order_submitted
    Ordering::OrderSubmitted.new(data: {order_id: order_id, order_number: order_number, customer_id: customer_id})
  end
  def order_expired
    Ordering::OrderExpired.new(data: {order_id: order_id})
  end
  def order_paid
    Ordering::OrderPaid.new(data: {order_id: order_id, transaction_id: transaction_id})
  end
  def payment_authorized
    Payments::PaymentAuthorized.new(data: {
      transaction_id: transaction_id,
      order_id: order_id
    })
  end
  def payment_released
    Payments::PaymentReleased.new(data: {
      transaction_id: transaction_id,
      order_id: order_id
    })
  end
end
And here is how you can subscribe the events to the process in the config of event store
    store.subscribe(PaymentProcess, to: [
      Ordering::OrderSubmitted,
      Ordering::OrderExpired,
      Ordering::OrderPaid,
      Payments::PaymentAuthorized,
      Payments::PaymentReleased,
    ])
Sagas are more about being able to compensate for failure in a process, for example backtracking in a process that has a predictable failure, or just allows this kind of action to be taken. PM in this case, are more useful to handle processes that are complicated, or large, but do not allow failures. For example: you book a hotel, book a car, and then try to book a flight, but something goes wrong there/you change your mind. You go back and cancel the car and hotel. Saga is the pattern to be used in this kind of scenario, not Process Manager.
tags: