SlideShare a Scribd company logo
Domain Modeling in Ruby on Rails
With introduction to Domain-Driven Design
Paweł Strzałkowski


p.strzalkowski@visuality.pl
Meet Alojzy
Alojzy Meets Rails
Alojzy Loves Rails
Business going off
• $$$


• so
fl
exible and easy!
Months going by...
Complexity kicks in
• Pricing


• Payments


• Shipments


• Returns


• Product Rankings


• Push Noti
fi
cations through Xcompany


• Email API in Ycompany


• Product Recommendations in Zcompany
Alojzy feels lost
• Model


• View


• Controller


... it's obvious that Rails is not enough
Alojzy has enough
If it looks and behaves like a CRUD....
• Create


• Read


• Update


• Delete
Big Ball of Mud
Domain-Driven Design
• software design approach focusing on modelling software to match
a domain according to input from that domain's experts
Domain-Driven Design
• software design approach focusing on modelling software to match
a domain according to input from that domain's experts
Domain-Driven Design
• software design approach focusing on modelling software to match
a domain according to input from that domain's experts
Strategic DDD Elements: Subdomain
Product Catalog
Orders
Shipping
Payments
Warehouse
Strategic DDD Elements: Bounded Context
Product Catalog
Orders
Shipping
Payments
Warehouse
Ecommerce


Context
Availability


Context
Product


Context
Bounded Context
Product Catalog
Orders
Shipping
Payments
Warehouse
Big ball of mud
Bounded Context
Product Catalog
Orders
Shipping
Payments
Warehouse
Product


Context
Order


Context
Billable


Context
Shipping


Context
Availability


Context
Strategic DDD Elements: Ubiquitous Language
Shipping


Context
Shipment
Product
Package
Address
Location Shipping
Ubiquitous Language
Shipping


Context
Shipment
Product
Package
Address
Location Shipping
shipment.update(expected_execution_at: 2.days.since)


shipment.plan_shipping(2.days.since)
Context Map
Identity and Access Context
Product Context
Availability Context
U U
U
D
D
D
OHS / PL OHS / PL
ACL
ACL
Alojzy listens...
Alojzy doubts
oh no....
Tactical DDD
• How to express a model of a domain


• Embrace object oriented programming
Tactical DDD Elements
• Entity


• Value Object


• Aggregate


• Repository


• Factory


• Service


• Domain Event
Tactical DDD Elements: Entity
An object de
fi
ned primarily by its identity is called an ENTITY.


- Eric Evans


• Identity


• Behavior
Tactical DDD Elements: Entity - Identity strategies
• user driven


• application generated


• persistence store generated


• outside
Tactical DDD Elements: Entity - Indispensable behavior
• domain objects have behavior essential to the context


• setter or getter for every primitive do not express behaviour
customer.change_phone_number("+48 123 555 123")


invoice.void(reason: "Client changed his mind")


account.activate
Tactical DDD Elements: Value Object
An object that represents a descriptive aspect of the domain with no
conceptual identity is called a VALUE OBJECT


- Eric Evans


• Represents value


• Immutable


• Comparable


• No identity
Tactical DDD Elements: Aggregate
An AGGREGATE is a cluster of associated objects that we treat as a unit
for the purpose of data changes.


- Eric Evans


• Aggregate Root


• Boundary


• Invariants
Aggregate Root


* invariants
Value Object
Entity
Value Object
Tactical DDD Elements: Aggregate
Address
Client


*max 2 addresses
Contact
Details
address = Addres.new(address_params)


client.add_address(address)
Alojzy doubts
But there is no place for that in Rails
Tactical DDD Elements: Value Object - IRL Example
class TrackEvent


def initialize(user_id:, event:)


self.user_id = user_id


self.event = event


end


def call


return false unless Configuration.enabled?


Client.track(


user_id: user_id,


event: event,


properties: properties


)


end


private


# ...


end
Tactical DDD Elements: Value Object - IRL Example
class TrackEvent


def initialize(user_id:, noun: nil, verb: nil)


self.user_id = user_id


self.event = "#{noun} #{verb}"


end


def call


return false unless Configuration.enabled?


Client.track(


user_id: user_id,


event: event,


properties: properties


)


end


private


# ...


end
Tactical DDD Elements: Value Object - IRL Example
class TrackEvent


def initialize(user_id:, noun: nil, verb: nil, event: nil)


self.user_id = user_id


self.event = event.presence || "#{noun} #{verb}"


end


def call


return false unless Configuration.enabled?


Client.track(


user_id: user_id,


event: event,


properties: properties


)


end


private


# ...


end
Tactical DDD Elements: Value Object - IRL Example
class Event


attr_reader :name


def initialize(name:)


@name = name.to_s


end


def self.combination_of(noun:, verb:)


new(name: "#{noun} #{verb}")


end


def ==(other)


name == other.name


end


end


Event.new(name: 'Job Created')


Event.combination_of(noun: 'Job', verb: 'Started')
Tactical DDD Elements: Value Object - IRL Example
class TrackEvent


def initialize(user_id:, event:)


self.user_id = user_id


self.event = event.name


end


def call


return false unless Configuration.enabled?


Client.track(


user_id: user_id,


event: event,


properties: properties


)


end


private


# ...


end
Tactical DDD Elements: Value Object - RoR Example
class Place < ApplicationRecord


# name, latitude, longitude


end
Tactical DDD Elements: Value Object - RoR Example
class Place < ApplicationRecord


# name, latitude, longitude


composed_of :coordinates


end
Tactical DDD Elements: Value Object - RoR Example
class Place < ApplicationRecord


# name, latitude, longitude


composed_of :coordinates,


class_name: 'Coordinates'


mapping: [%w[latitude latitude], %w[longitude longitude]]


end


class Coordinates


attr_reader :latitude, :longitude


def initialize(latitude, longitude)


@latitude = latitude


@longitude = longitude


end


def ==(other)


latitude == other.latitude && longitude == other.longitude


end


end
Alojzy listens
Interesting...
Domain Modeling in Ruby on Rails - Excercise
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone


• When a customer contacts us, we use their phone numbers to
fi
nd
the records in our books
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone


• When a customer contacts us, we use their phone numbers to
fi
nd
the records in our books


• Each customer has to have a name and a physical address
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone


• When a customer contacts us, we use their phone numbers to
fi
nd
the records in our books


• Each customer has to have a name and a physical address


• Birthdays are very important, we use them for marketing
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone


• When a customer contacts us, we use their phone numbers to
fi
nd
the records in our books


• Each customer has to have a name and a physical address


• Birthdays are very important, we use them for marketing


• We have a loyalty program based on the amount of time customer is
with us, we need to know when a customer was added
Domain Modeling in Ruby on Rails - Excercise
• Customers may be contacted through an email or a phone


• When a customer contacts us, we use their phone numbers to
fi
nd
the records in our books


• Each customer has to have a name and a physical address


• Birthdays are very important, we use them for marketing


• We have a loyalty program based on the amount of time customer is
with us, we need to know when a customer was added


• Customers get into interactions; they can rate others with a positive,
neutral or a negative mark, there can be one rate for one customer
Excercise - ORM-driven
Excercise - ORM-driven
class Customer < ApplicationRecord


validates :city, :street, :name, :birthday, presence: true


validates_with PhoneSyntaxValidator


validates :phone, uniqueness: true


validates_with EmailSyntaxValidator


validates :email, uniqueness: true


validate :birthday_in_the_past


has_many :rates


private


def birthday_in_the_past


errors.add(:birthday, "is in the future") if birthday&.future?


end


end


class Rate < ApplicationRecord


belongs_to :customer


belongs_to :rated_customer, class_name: 'Customer'


end
Excercise - With value objects
Excercise - With value objects
class Address


attr_reader :city, :street


def initialize(city, street)


@city = city


@street = street


end


end


class PersonalInformation


attr_reader :name, :birthday


def initialize(name, birthday)


@name = name


@birthday = birthday


end


end


class Rate < ApplicationRecord


belongs_to :customer


belongs_to :rated_customer,


class_name: 'Customer'


end


class Customer < ApplicationRecord


composed_of :address,


class_name: 'Address',


mapping: [%w[city city], %w[street street]]


validates_with AddressValidator


composed_of :personal_information,


class_name: 'PersonalInformation',


mapping: [


%w[name name], %w[birthday birthday]


]


validates_with PersonalInformationValidator


validates_with PhoneSyntaxValidator


validates :phone, uniqueness: true


validates_with EmailSyntaxValidator


validates :email, uniqueness: true


has_many :rates


end
Excercise - Highlighting identity
class Address


attr_reader :city, :street


def initialize(city, street)


@city = city


@street = street


end


end


class PersonalInformation


attr_reader :name, :birthday, :email


def initialize(name, birthday, email)


@name = name


@birthday = birthday


@email = email


end


end


class Rate < ApplicationRecord


belongs_to :customer


belongs_to :rated_customer,


class_name: 'Customer'


end
class Customer < ApplicationRecord


composed_of :address,


class_name: 'Address',


mapping: [%w[city city], %w[street street]]


validates_with AddressValidator


composed_of :personal_information,


class_name: 'PersonalInformation',


mapping: [


%w[name name], %w[birthday birthday], %[email email]


]


validates_with PersonalInformationValidator


validates_with PhoneSyntaxValidator


validates :phone, uniqueness: true


has_many :rates


end
Excercise - Adding behavior
class Customer < ApplicationRecord


AlreadyRated = Class.new(StandardError)


CannotRateSelf = Class.new(StandardError)


# ...


has_many :rates


def give_negative_rate(other) = give_rate(other, -1)


def give_neutral_rate(other) = give_rate(other, 0)


def give_positive_rate(other) = give_rate(other, 1)


private


def give_rate(other, mark)


raise CannotRateSelf if other == self


raise AlreadyRated if rates.any? { |rate| rate.rated_customer == other }


rates.build(rated_customer: other, mark: mark)


end


end
Excercise - Ugly service
module Customers


class RateService


MarkOutOfBounds = Class.new(StandardError)


AlreadyMarked = Class.new(StandardError)


CannotRateSelf = Class.new(StandardError)


def initialize(customer_id, rated_customer_id, mark)


@customer_id = customer_id


@rated_customer_id = rated_customer_id


@mark = mark


end


def call


raise CannotRateSelf if customer_id == rated_customer_id


customer = Customer.find(@customer_id)


rated_customer_id = Customer.find(@rated_customer_id)


raise MarkOutOfBounds unless [-1, 0, 1].include?(mark)


alread_rated = customer.rates.any? { |rate| rate.rated_customer == rated_customer }


raise AlreadyMarked if alread_rated


customer.rates.build(marked: other, mark: mark)


customer.save


end


end


end
Excercise - Intention revealing service
module Customers


class GivePositiveRateService


def initialize(customer_id, rated_customer_id)


@customer_id = customer_id


@rated_customer_id = rated_customer_id


end


def call


customer = Customer.find(@customer_id)


rated_customer = Customer.find(@rated_customer_id)


customer.give_positive_mark(rated_customer)


customer.save


end


end


end
Excercise - aggregate implementation
module Customers


class GivePositiveRateService


def initialize(customer_id, rated_customer_id)


@customer_id = customer_id


@rated_customer_id = rated_customer_id


end


def call


customer = Customer.find(@customer_id)


rated_customer = Customer.find(@rated_customer_id)


customer.give_positive_mark(rated_customer)


customer.save


end


end


end
Excercise - aggregate implementation
module Customers


class GivePositiveRateService


def initialize(customer_id, rated_customer_id)


@customer_id = customer_id


@rated_customer_id = rated_customer_id


end


def call


customer = Customer.find(@customer_id)


rated_customer = Customer.find(@rated_customer_id)


customer.give_positive_mark(rated_customer)


customer.save


end


end


end


Add optimistic locking
add_column :customers, :lock_version, :integer
We did it!
• Modelled the domain according to knowledge of a domain expert


• Created expressive and intention revealing code


• Guaranteed safety in concurrent operations
How?
• Avoided ORM-based design


• Listened to the domain expert


• Spent minimal time thinking
The job is not done
• Customer model is responsible for handling:


• address used for package delivery


• birthdays used for marketing actions


• email used for transactional email
The job is not done
• Customer model is responsible for handling:


• address used for package delivery


• birthdays used for marketing actions


• email used for transactional email
But that's a story for another time...
References
• Introduction to DDD in Ruby on Rails


• https://www.visuality.pl/posts/introduction-to-ddd-in-ruby-on-rails


• GitHub Repository with used examples


• https://github.com/pstrzalk/ddd-in-ruby-on-rails
Questions?
Comments? Criticism?
Paweł Strzałkowski


p.strzalkowski@visuality.pl


@realPawelS

More Related Content

Similar to Introduction to Domain-Driven Design in Ruby on Rails

Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a TimeWebinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
MongoDB
 
Test scenarios for sending & receiving emails
Test scenarios for sending & receiving emailsTest scenarios for sending & receiving emails
Test scenarios for sending & receiving emails
Morpheous Algan
 

Similar to Introduction to Domain-Driven Design in Ruby on Rails (20)

E Commerce Web Design Service Proposal PowerPoint Presentation Slides
E Commerce Web Design Service Proposal PowerPoint Presentation SlidesE Commerce Web Design Service Proposal PowerPoint Presentation Slides
E Commerce Web Design Service Proposal PowerPoint Presentation Slides
 
Using AI for Providing Insights and Recommendations on Activity Data Alexis R...
Using AI for Providing Insights and Recommendations on Activity Data Alexis R...Using AI for Providing Insights and Recommendations on Activity Data Alexis R...
Using AI for Providing Insights and Recommendations on Activity Data Alexis R...
 
Micro-service architectures with Gilmour
Micro-service architectures with GilmourMicro-service architectures with Gilmour
Micro-service architectures with Gilmour
 
Done in 60 seconds - Creating Web 2.0 applications made easy
Done in 60 seconds - Creating Web 2.0 applications made easyDone in 60 seconds - Creating Web 2.0 applications made easy
Done in 60 seconds - Creating Web 2.0 applications made easy
 
Up Your Freelancing Game
Up Your Freelancing GameUp Your Freelancing Game
Up Your Freelancing Game
 
Online Store Website Design Proposal PowerPoint Presentation Slides
Online Store Website Design Proposal PowerPoint Presentation SlidesOnline Store Website Design Proposal PowerPoint Presentation Slides
Online Store Website Design Proposal PowerPoint Presentation Slides
 
How to Price Recurring Revenue Services in Your Mobile App Business
How to Price Recurring Revenue Services in Your Mobile App BusinessHow to Price Recurring Revenue Services in Your Mobile App Business
How to Price Recurring Revenue Services in Your Mobile App Business
 
Restructuring rails
Restructuring railsRestructuring rails
Restructuring rails
 
Ankur Bajad
Ankur BajadAnkur Bajad
Ankur Bajad
 
Angular js firebase-preso
Angular js firebase-presoAngular js firebase-preso
Angular js firebase-preso
 
Session 3a The SF SaaS Framework
Session 3a  The SF SaaS FrameworkSession 3a  The SF SaaS Framework
Session 3a The SF SaaS Framework
 
Vishnu Updated
Vishnu UpdatedVishnu Updated
Vishnu Updated
 
E commerce Proposal Template PowerPoint Presentation Slides
E commerce Proposal Template PowerPoint Presentation SlidesE commerce Proposal Template PowerPoint Presentation Slides
E commerce Proposal Template PowerPoint Presentation Slides
 
Identifying Pricing Request Emails Using Apache Spark and Machine Learning
Identifying Pricing Request Emails Using Apache Spark and Machine LearningIdentifying Pricing Request Emails Using Apache Spark and Machine Learning
Identifying Pricing Request Emails Using Apache Spark and Machine Learning
 
The Fine Art of Schema Design in MongoDB: Dos and Don'ts
The Fine Art of Schema Design in MongoDB: Dos and Don'tsThe Fine Art of Schema Design in MongoDB: Dos and Don'ts
The Fine Art of Schema Design in MongoDB: Dos and Don'ts
 
Refactoring for microservices
Refactoring for microservicesRefactoring for microservices
Refactoring for microservices
 
Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a TimeWebinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
Webinar: Realizing Omni-Channel Retailing with MongoDB - One Step at a Time
 
#CNX14 - Intro to Force
#CNX14 - Intro to Force#CNX14 - Intro to Force
#CNX14 - Intro to Force
 
2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD Workshop2011-02-03 LA RubyConf Rails3 TDD Workshop
2011-02-03 LA RubyConf Rails3 TDD Workshop
 
Test scenarios for sending & receiving emails
Test scenarios for sending & receiving emailsTest scenarios for sending & receiving emails
Test scenarios for sending & receiving emails
 

More from Visuality

More from Visuality (20)

3 issues that made 30 test workers take 40 minutes
3 issues that made 30 test workers take 40 minutes3 issues that made 30 test workers take 40 minutes
3 issues that made 30 test workers take 40 minutes
 
Czego nie robić przy pisaniu testów
Czego nie robić przy pisaniu testówCzego nie robić przy pisaniu testów
Czego nie robić przy pisaniu testów
 
Active Record .includes - do you use it consciously?
Active Record .includes - do you use it consciously?Active Record .includes - do you use it consciously?
Active Record .includes - do you use it consciously?
 
Introduction to Event Storming
Introduction to Event StormingIntroduction to Event Storming
Introduction to Event Storming
 
Jak programowanie może pomóc na co dzień?
Jak programowanie może pomóc na co dzień?Jak programowanie może pomóc na co dzień?
Jak programowanie może pomóc na co dzień?
 
SVG Overview - How To Draw, Use and Animate
SVG Overview - How To Draw, Use and AnimateSVG Overview - How To Draw, Use and Animate
SVG Overview - How To Draw, Use and Animate
 
How To Migrate a Rails App From a Dedicated Server Into Cloud Environment? - ...
How To Migrate a Rails App From a Dedicated Server Into Cloud Environment? - ...How To Migrate a Rails App From a Dedicated Server Into Cloud Environment? - ...
How To Migrate a Rails App From a Dedicated Server Into Cloud Environment? - ...
 
How to use AWS SES with Lambda 
in Ruby on Rails application - Michał Łęcicki
How to use AWS SES with Lambda 
in Ruby on Rails application - Michał ŁęcickiHow to use AWS SES with Lambda 
in Ruby on Rails application - Michał Łęcicki
How to use AWS SES with Lambda 
in Ruby on Rails application - Michał Łęcicki
 
What is NOT machine learning - Burak Aybar
What is NOT machine learning - Burak AybarWhat is NOT machine learning - Burak Aybar
What is NOT machine learning - Burak Aybar
 
Do you really need to reload?
Do you really need to reload?Do you really need to reload?
Do you really need to reload?
 
How to check valid email? Find using regex(p?)
How to check valid email? Find using regex(p?)How to check valid email? Find using regex(p?)
How to check valid email? Find using regex(p?)
 
Fantastic stresses and where to find them
Fantastic stresses and where to find themFantastic stresses and where to find them
Fantastic stresses and where to find them
 
Fuzzy search in Ruby
Fuzzy search in RubyFuzzy search in Ruby
Fuzzy search in Ruby
 
Rfc process in visuality
Rfc process in visualityRfc process in visuality
Rfc process in visuality
 
GraphQL in Ruby on Rails - basics
GraphQL in Ruby on Rails - basicsGraphQL in Ruby on Rails - basics
GraphQL in Ruby on Rails - basics
 
Consumer Driven Contracts
Consumer Driven ContractsConsumer Driven Contracts
Consumer Driven Contracts
 
How do we use CircleCi in Laterallink?
How do we use CircleCi in Laterallink?How do we use CircleCi in Laterallink?
How do we use CircleCi in Laterallink?
 
React Native - Short introduction
React Native - Short introductionReact Native - Short introduction
React Native - Short introduction
 
Risk in project management
Risk in project managementRisk in project management
Risk in project management
 
Ruby formatters
Ruby formattersRuby formatters
Ruby formatters
 

Recently uploaded

Recently uploaded (20)

Intro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджераIntro in Product Management - Коротко про професію продакт менеджера
Intro in Product Management - Коротко про професію продакт менеджера
 
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
 
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
Kubernetes & AI - Beauty and the Beast !?! @KCD Istanbul 2024
 
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptxWSO2CONMay2024OpenSourceConferenceDebrief.pptx
WSO2CONMay2024OpenSourceConferenceDebrief.pptx
 
Powerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara LaskowskaPowerful Start- the Key to Project Success, Barbara Laskowska
Powerful Start- the Key to Project Success, Barbara Laskowska
 
AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101AI presentation and introduction - Retrieval Augmented Generation RAG 101
AI presentation and introduction - Retrieval Augmented Generation RAG 101
 
Connecting the Dots in Product Design at KAYAK
Connecting the Dots in Product Design at KAYAKConnecting the Dots in Product Design at KAYAK
Connecting the Dots in Product Design at KAYAK
 
UiPath Test Automation using UiPath Test Suite series, part 2
UiPath Test Automation using UiPath Test Suite series, part 2UiPath Test Automation using UiPath Test Suite series, part 2
UiPath Test Automation using UiPath Test Suite series, part 2
 
Knowledge engineering: from people to machines and back
Knowledge engineering: from people to machines and backKnowledge engineering: from people to machines and back
Knowledge engineering: from people to machines and back
 
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi IbrahimzadeFree and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
Free and Effective: Making Flows Publicly Accessible, Yumi Ibrahimzade
 
What's New in Teams Calling, Meetings and Devices April 2024
What's New in Teams Calling, Meetings and Devices April 2024What's New in Teams Calling, Meetings and Devices April 2024
What's New in Teams Calling, Meetings and Devices April 2024
 
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered QualitySoftware Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
Software Delivery At the Speed of AI: Inflectra Invests In AI-Powered Quality
 
ODC, Data Fabric and Architecture User Group
ODC, Data Fabric and Architecture User GroupODC, Data Fabric and Architecture User Group
ODC, Data Fabric and Architecture User Group
 
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
SOQL 201 for Admins & Developers: Slice & Dice Your Org’s Data With Aggregate...
 
In-Depth Performance Testing Guide for IT Professionals
In-Depth Performance Testing Guide for IT ProfessionalsIn-Depth Performance Testing Guide for IT Professionals
In-Depth Performance Testing Guide for IT Professionals
 
Motion for AI: Creating Empathy in Technology
Motion for AI: Creating Empathy in TechnologyMotion for AI: Creating Empathy in Technology
Motion for AI: Creating Empathy in Technology
 
A Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System StrategyA Business-Centric Approach to Design System Strategy
A Business-Centric Approach to Design System Strategy
 
"Impact of front-end architecture on development cost", Viktor Turskyi
"Impact of front-end architecture on development cost", Viktor Turskyi"Impact of front-end architecture on development cost", Viktor Turskyi
"Impact of front-end architecture on development cost", Viktor Turskyi
 
Enterprise Security Monitoring, And Log Management.
Enterprise Security Monitoring, And Log Management.Enterprise Security Monitoring, And Log Management.
Enterprise Security Monitoring, And Log Management.
 
Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
Exploring UiPath Orchestrator API: updates and limits in 2024 🚀Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
Exploring UiPath Orchestrator API: updates and limits in 2024 🚀
 

Introduction to Domain-Driven Design in Ruby on Rails

  • 1. Domain Modeling in Ruby on Rails With introduction to Domain-Driven Design Paweł Strzałkowski p.strzalkowski@visuality.pl
  • 5. Business going off • $$$ • so fl exible and easy!
  • 7. Complexity kicks in • Pricing • Payments • Shipments • Returns • Product Rankings • Push Noti fi cations through Xcompany • Email API in Ycompany • Product Recommendations in Zcompany
  • 8. Alojzy feels lost • Model • View • Controller ... it's obvious that Rails is not enough
  • 10. If it looks and behaves like a CRUD.... • Create • Read • Update • Delete
  • 11. Big Ball of Mud
  • 12. Domain-Driven Design • software design approach focusing on modelling software to match a domain according to input from that domain's experts
  • 13. Domain-Driven Design • software design approach focusing on modelling software to match a domain according to input from that domain's experts
  • 14. Domain-Driven Design • software design approach focusing on modelling software to match a domain according to input from that domain's experts
  • 15. Strategic DDD Elements: Subdomain Product Catalog Orders Shipping Payments Warehouse
  • 16. Strategic DDD Elements: Bounded Context Product Catalog Orders Shipping Payments Warehouse Ecommerce Context Availability Context Product Context
  • 19. Strategic DDD Elements: Ubiquitous Language Shipping Context Shipment Product Package Address Location Shipping
  • 21. Context Map Identity and Access Context Product Context Availability Context U U U D D D OHS / PL OHS / PL ACL ACL
  • 24. Tactical DDD • How to express a model of a domain • Embrace object oriented programming
  • 25. Tactical DDD Elements • Entity • Value Object • Aggregate • Repository • Factory • Service • Domain Event
  • 26. Tactical DDD Elements: Entity An object de fi ned primarily by its identity is called an ENTITY. - Eric Evans • Identity • Behavior
  • 27. Tactical DDD Elements: Entity - Identity strategies • user driven • application generated • persistence store generated • outside
  • 28. Tactical DDD Elements: Entity - Indispensable behavior • domain objects have behavior essential to the context • setter or getter for every primitive do not express behaviour customer.change_phone_number("+48 123 555 123") invoice.void(reason: "Client changed his mind") account.activate
  • 29. Tactical DDD Elements: Value Object An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT - Eric Evans • Represents value • Immutable • Comparable • No identity
  • 30. Tactical DDD Elements: Aggregate An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. - Eric Evans • Aggregate Root • Boundary • Invariants Aggregate Root * invariants Value Object Entity Value Object
  • 31. Tactical DDD Elements: Aggregate Address Client *max 2 addresses Contact Details address = Addres.new(address_params) client.add_address(address)
  • 32. Alojzy doubts But there is no place for that in Rails
  • 33. Tactical DDD Elements: Value Object - IRL Example class TrackEvent def initialize(user_id:, event:) self.user_id = user_id self.event = event end def call return false unless Configuration.enabled? Client.track( user_id: user_id, event: event, properties: properties ) end private # ... end
  • 34. Tactical DDD Elements: Value Object - IRL Example class TrackEvent def initialize(user_id:, noun: nil, verb: nil) self.user_id = user_id self.event = "#{noun} #{verb}" end def call return false unless Configuration.enabled? Client.track( user_id: user_id, event: event, properties: properties ) end private # ... end
  • 35. Tactical DDD Elements: Value Object - IRL Example class TrackEvent def initialize(user_id:, noun: nil, verb: nil, event: nil) self.user_id = user_id self.event = event.presence || "#{noun} #{verb}" end def call return false unless Configuration.enabled? Client.track( user_id: user_id, event: event, properties: properties ) end private # ... end
  • 36. Tactical DDD Elements: Value Object - IRL Example class Event attr_reader :name def initialize(name:) @name = name.to_s end def self.combination_of(noun:, verb:) new(name: "#{noun} #{verb}") end def ==(other) name == other.name end end Event.new(name: 'Job Created') Event.combination_of(noun: 'Job', verb: 'Started')
  • 37. Tactical DDD Elements: Value Object - IRL Example class TrackEvent def initialize(user_id:, event:) self.user_id = user_id self.event = event.name end def call return false unless Configuration.enabled? Client.track( user_id: user_id, event: event, properties: properties ) end private # ... end
  • 38. Tactical DDD Elements: Value Object - RoR Example class Place < ApplicationRecord # name, latitude, longitude end
  • 39. Tactical DDD Elements: Value Object - RoR Example class Place < ApplicationRecord # name, latitude, longitude composed_of :coordinates end
  • 40. Tactical DDD Elements: Value Object - RoR Example class Place < ApplicationRecord # name, latitude, longitude composed_of :coordinates, class_name: 'Coordinates' mapping: [%w[latitude latitude], %w[longitude longitude]] end class Coordinates attr_reader :latitude, :longitude def initialize(latitude, longitude) @latitude = latitude @longitude = longitude end def ==(other) latitude == other.latitude && longitude == other.longitude end end
  • 42. Domain Modeling in Ruby on Rails - Excercise
  • 43. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone
  • 44. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone • When a customer contacts us, we use their phone numbers to fi nd the records in our books
  • 45. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone • When a customer contacts us, we use their phone numbers to fi nd the records in our books • Each customer has to have a name and a physical address
  • 46. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone • When a customer contacts us, we use their phone numbers to fi nd the records in our books • Each customer has to have a name and a physical address • Birthdays are very important, we use them for marketing
  • 47. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone • When a customer contacts us, we use their phone numbers to fi nd the records in our books • Each customer has to have a name and a physical address • Birthdays are very important, we use them for marketing • We have a loyalty program based on the amount of time customer is with us, we need to know when a customer was added
  • 48. Domain Modeling in Ruby on Rails - Excercise • Customers may be contacted through an email or a phone • When a customer contacts us, we use their phone numbers to fi nd the records in our books • Each customer has to have a name and a physical address • Birthdays are very important, we use them for marketing • We have a loyalty program based on the amount of time customer is with us, we need to know when a customer was added • Customers get into interactions; they can rate others with a positive, neutral or a negative mark, there can be one rate for one customer
  • 50. Excercise - ORM-driven class Customer < ApplicationRecord validates :city, :street, :name, :birthday, presence: true validates_with PhoneSyntaxValidator validates :phone, uniqueness: true validates_with EmailSyntaxValidator validates :email, uniqueness: true validate :birthday_in_the_past has_many :rates private def birthday_in_the_past errors.add(:birthday, "is in the future") if birthday&.future? end end class Rate < ApplicationRecord belongs_to :customer belongs_to :rated_customer, class_name: 'Customer' end
  • 51. Excercise - With value objects
  • 52. Excercise - With value objects class Address attr_reader :city, :street def initialize(city, street) @city = city @street = street end end class PersonalInformation attr_reader :name, :birthday def initialize(name, birthday) @name = name @birthday = birthday end end class Rate < ApplicationRecord belongs_to :customer belongs_to :rated_customer, class_name: 'Customer' end class Customer < ApplicationRecord composed_of :address, class_name: 'Address', mapping: [%w[city city], %w[street street]] validates_with AddressValidator composed_of :personal_information, class_name: 'PersonalInformation', mapping: [ %w[name name], %w[birthday birthday] ] validates_with PersonalInformationValidator validates_with PhoneSyntaxValidator validates :phone, uniqueness: true validates_with EmailSyntaxValidator validates :email, uniqueness: true has_many :rates end
  • 53. Excercise - Highlighting identity class Address attr_reader :city, :street def initialize(city, street) @city = city @street = street end end class PersonalInformation attr_reader :name, :birthday, :email def initialize(name, birthday, email) @name = name @birthday = birthday @email = email end end class Rate < ApplicationRecord belongs_to :customer belongs_to :rated_customer, class_name: 'Customer' end class Customer < ApplicationRecord composed_of :address, class_name: 'Address', mapping: [%w[city city], %w[street street]] validates_with AddressValidator composed_of :personal_information, class_name: 'PersonalInformation', mapping: [ %w[name name], %w[birthday birthday], %[email email] ] validates_with PersonalInformationValidator validates_with PhoneSyntaxValidator validates :phone, uniqueness: true has_many :rates end
  • 54. Excercise - Adding behavior class Customer < ApplicationRecord AlreadyRated = Class.new(StandardError) CannotRateSelf = Class.new(StandardError) # ... has_many :rates def give_negative_rate(other) = give_rate(other, -1) def give_neutral_rate(other) = give_rate(other, 0) def give_positive_rate(other) = give_rate(other, 1) private def give_rate(other, mark) raise CannotRateSelf if other == self raise AlreadyRated if rates.any? { |rate| rate.rated_customer == other } rates.build(rated_customer: other, mark: mark) end end
  • 55. Excercise - Ugly service module Customers class RateService MarkOutOfBounds = Class.new(StandardError) AlreadyMarked = Class.new(StandardError) CannotRateSelf = Class.new(StandardError) def initialize(customer_id, rated_customer_id, mark) @customer_id = customer_id @rated_customer_id = rated_customer_id @mark = mark end def call raise CannotRateSelf if customer_id == rated_customer_id customer = Customer.find(@customer_id) rated_customer_id = Customer.find(@rated_customer_id) raise MarkOutOfBounds unless [-1, 0, 1].include?(mark) alread_rated = customer.rates.any? { |rate| rate.rated_customer == rated_customer } raise AlreadyMarked if alread_rated customer.rates.build(marked: other, mark: mark) customer.save end end end
  • 56. Excercise - Intention revealing service module Customers class GivePositiveRateService def initialize(customer_id, rated_customer_id) @customer_id = customer_id @rated_customer_id = rated_customer_id end def call customer = Customer.find(@customer_id) rated_customer = Customer.find(@rated_customer_id) customer.give_positive_mark(rated_customer) customer.save end end end
  • 57. Excercise - aggregate implementation module Customers class GivePositiveRateService def initialize(customer_id, rated_customer_id) @customer_id = customer_id @rated_customer_id = rated_customer_id end def call customer = Customer.find(@customer_id) rated_customer = Customer.find(@rated_customer_id) customer.give_positive_mark(rated_customer) customer.save end end end
  • 58. Excercise - aggregate implementation module Customers class GivePositiveRateService def initialize(customer_id, rated_customer_id) @customer_id = customer_id @rated_customer_id = rated_customer_id end def call customer = Customer.find(@customer_id) rated_customer = Customer.find(@rated_customer_id) customer.give_positive_mark(rated_customer) customer.save end end end Add optimistic locking add_column :customers, :lock_version, :integer
  • 59. We did it! • Modelled the domain according to knowledge of a domain expert • Created expressive and intention revealing code • Guaranteed safety in concurrent operations How? • Avoided ORM-based design • Listened to the domain expert • Spent minimal time thinking
  • 60. The job is not done • Customer model is responsible for handling: • address used for package delivery • birthdays used for marketing actions • email used for transactional email
  • 61. The job is not done • Customer model is responsible for handling: • address used for package delivery • birthdays used for marketing actions • email used for transactional email But that's a story for another time...
  • 62. References • Introduction to DDD in Ruby on Rails • https://www.visuality.pl/posts/introduction-to-ddd-in-ruby-on-rails • GitHub Repository with used examples • https://github.com/pstrzalk/ddd-in-ruby-on-rails