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:
- blog.pragmatists.com/refactoring-from-anemic-model-to-ddd-880d3dd3d45f (refactoring anemic models)
- slides.com/imanel/from-ddd-to-event-sourcing (this presentation)
Questions?
From DDD to Event Sourcing
By Bernard Potocki
From DDD to Event Sourcing
- 1,704