From

Domain-Driven Design to

Event Sourcing

What is DDD?

Building blocks:

  • Entities
  • Value Objects
  • Services
  • Modules
  • Aggregates
  • Factories
  • Repositories

Bounded Context

Bounded Context

class MessagesController < ApplicationController
  def show
    message = Message.for_user(current_user).not_deleted.find params[:id]
    message.update_attribute(read_at: Time.now) if current_user == message.to
    render json: message
  end

  def create
    message = Message.new

    message.attributes = params.require(:message).permit(:subject, :text, :to)
    message.from = current_user

    if message.save
      render json: message, status: :created
    else
      render json: message.errors, status: :bad_request
    end
  end
end

Bounded Context

class Message < ActiveRecord::Base
  soft_delete

  belongs_to :from, class_name: 'User', foreign_key: :from_user_id
  belongs_to :to,   class_name: 'User', foreign_key: :to_user_id

  validates :text, :from, :to, presence: true

  after_create :notify_user
  after_save   :count_messages

  scope :for_user, -> (user) {
    where('from_user_id = ? OR to_user_id = ?', user.id, user.id)
  }

  def notify_user
    return unless to.receive_notifications
    UserMailer.message_notification(to, self).deliver_later
  end

  def count_messages
    count = to.incoming_messages.where(read_at: nil, deleted_at: nil).count
    to.update_attributes(unread_messages_count: count)
  end
end

Let's add some Services!

CreateMessageService

class MessagesController < ApplicationController
  def show
    message = Message.for_user(current_user).not_deleted.find params[:id]
    message.update_attribute(read_at: Time.now) if current_user == message.to
    render json: message
  end

  def create
    message = MessageCreatorService.call(current_user, permitted_params)

    if message.valid?
      render json: message.resource, status: :created
    else
      render json: message.errors, status: :bad_request
    end
  end
end

CreateMessageService

class Message < ActiveRecord::Base
  soft_delete

  belongs_to :from, class_name: 'User', foreign_key: :from_user_id
  belongs_to :to,   class_name: 'User', foreign_key: :to_user_id

  validates :text, :from, :to, presence: true

  scope :to_user, -> (user) { where(to_user_id: user.id) }
  scope :from_user, -> (user) { where(to_user_id: user.id) }
  scope :for_user, -> (user) { to_user(user).or(from_user(user)) }
  scope :unread, -> { where(read_at: nil) }
  scope :not_deleted, -> { where(deleted_at: nil) }
end

CreateMessageService

module MessageCreatorService
  attr_reader :resource
  delegate :errors, :valid?, to: :resource

  def call(current_user, params)
    @resource = Message.new
    @resource.attributes = params.require(:message).permit(:subject, :text, :to)
    @resource.from = current_user
    if @resource.save
      notify_user
      count_messages
    end
  end

  def notify_user
    return unless @resource.to.receive_notifications
    UserMailer.message_notification(@resource.to, @resource).deliver_later
  end

  def count_messages
    count = Message.to_user(@resource.to).unread.not_deleted.count
    @resource.to.update_attributes(unread_messages_count: count)
  end
end

Anemic Domain Model

Modules

class MessagesController < ApplicationController

  def show
    message = Domain::Message.get(current_user, id: params[:id])
    render json: message
  end

  def create
    message = Domain::Message.create(current_user, params)

    render json: message, status: :created
  rescue Domain::ParamsError => error
    render json: error.message, status: :bad_request
  end

  def delete
    Domain::Message.delete(current_user, id: params[:id])
    head :ok
  end

end

Modules

module Domain::Messages
  extend self

  def create(current_user, params)
    message = Message.new(params)
    message.from_user_id = current_user.id

    if message.save
      notify_user(message.to_user_id, message.id)
      count_messages(message.to_user_id)
    else
      raise ParamsError, message.errors
    end
  end

  def notify_user(user_id, message_id)
    Domain::Mailer.send_message_notification(user_id, message_id)
  end

  def count_messages(user_id)
    count = Message.to_user(user_id).unread.not_deleted.count
    Domain::User.update_counter(user_id, :unread_messages, count)
  end
end

Modules

class Message < ActiveRecord::Base
  soft_delete

  validates :text, :from_user_id, :to_user_id, presence: true

  scope :to_user, -> (user_id) { where(to_user_id: user_id) }
  scope :from_user, -> (user_id) { where(to_user_id: user_id) }
  scope :unread, -> { where(read_at: nil) }
  scope :not_deleted, -> { where(deleted_at: nil) }
end

Ubiquitous Language

Domain

Expert

Technical

Expert

?

Ubiquitous Language

module Domain::Messages
  extend self

  def list(current_user, params)
    # Some code
  end

  def info(current_user, params)
    # Some code
  end

  def send(current_user, params)
    # Some code
  end

  def archive(current_user, params)
    # Some code
  end
end

Ubiquitous Language

module Domain::Messages
  extend self

  def list(as_user, params)
    # Some code
  end

  def info(as_user, params)
    # Some code
  end

  def send(as_user, params)
    # Some code
  end

  def archive(as_user, params)
    # Some code
  end
end

Ubiquitous Language

module Domain::Messages
  extend self

  def list(as_user, to_user: nil, offset: 0, limit: 10)
    # Some code
  end

  def info(as_user, message_id:)
    # Some code
  end

  def send(as_user, to_user:, with_text:)
    # Some code
  end

  def archive(as_user, message_id:)
    # Some code
  end
end

REST           vs       RPC      

POST /users/501/messages HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"text": "Hello!"}
POST /messages.send HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"to_user": 501, "text": "Hello!"}
PUT /messages/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"archived": true}
POST /messages.archive HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"message_id": 123}
POST /messages/123/archive HTTP/1.1
Host: api.example.com
Content-Type: application/json

{}

Eventual Consistency

Event Sourcing

Event Sourcing

module Domain::Messages

  def create(as_user, to_user:, with_text:)
    params = {

      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    message = Message.new(params)

    unless message.save
      raise(ParamsError, message.errors)
    end

    EventBus.emit(:message_created, params.merge(id: message.id))
    message
  end

end

Event Sourcing

module Domain::Messages

  def create(as_user, to_user:, with_text:)
    params = {
      id: SecureRandom.uuid,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    message = Message.new(params)

    unless message.save
      raise(ParamsError, message.errors)
    end

    EventBus.emit(:message_created, params)
    message
  end

end

Event Sourcing

module Domain::Messages

  def create(as_user, to_user:, with_text:)
    params = {
      id: SecureRandom.uuid,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params) # i.e. Dry-Validation?
      raise(ParamsError, errors)
    end

    message = Message.create(params)

    EventBus.emit(:message_created, params)
    message
  end

end

CQRS

Simple rules:

  • Query:
    • has no side effects
  • Command
    • don't query database
    • returns nothing

CQRS

module Domain::Messages

  def create(as_user, to_user:, with_text:)
    params = {
      id: SecureRandom.uuid,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params)
      raise(ParamsError, errors)
    end

    message = Message.create(params)

    EventBus.emit(:message_created, params)
    message.id
  end

end

CQRS

module Domain::Messages

  def create(as_user, to_user:, with_text:)
    params = {
      id: SecureRandom.uuid,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params)
      raise(ParamsError, errors)
    end

    Message.create(params)

    EventBus.emit(:message_created, params)
    params[:id]
  end

end

CQRS

module Domain::Messages

  def create(as_user, message_id:, to_user:, with_text:)
    params = {
      id: message_id,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params)
      raise(ParamsError, errors)
    end

    Message.create(params)

    EventBus.emit(:message_created, params)
    nil
  end

end

Putting it all together

CQRS

module Domain::Messages

  def create(as_user, message_id:, to_user:, with_text:)
    params = {
      id: message_id,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params)
      raise(ParamsError, errors)
    end

    Message.create(params)

    EventBus.emit(:message_created, params)
    nil
  end

end

CQRS

module Domain::Messages

  def create(as_user, message_id:, to_user:, with_text:)
    params = {
      id: message_id,
      from_user_id: as_user.id,
      to_user_id: to_user,
      text: with_text
    }

    if errors = validate(params)
      raise(ParamsError, errors)
    end



    EventBus.emit(:message_created, params)
    nil
  end

end

Saga Pattern

Orchestrator Pattern

BPMN

Process Managers

BDD

most important thing...

break the rules!

More reads:

Questions?

From DDD to Event Sourcing

By Bernard Potocki

From DDD to Event Sourcing

  • 1,704