Build Complex Domains in Rails

7ac2a66004880abc5032b1894bb12fda?s=47 Mike AbiEzzi
September 25, 2014

Build Complex Domains in Rails

Rails models are simple, but your domain’s models might not be as simple as Rails would like them to be.

Modeling large, complex domains "the Rails way” can cause some serious pain. Ruby and Rails are supposed to make developers happy. Let's not allow “the Rails way” and complex domains to take the “happy” out of ruby development.

Join me as I’ll walk through a set of easy to implement Domain Driven Design (DDD) pointers. The goal is to make sure your model’s business logic stay under control no matter how complex your domain is or gets. Your application will be able to sustain steady growth and dramatically reduce business rule related defects, all the while, staying easy to grok.

I'll walk you through:

How communicating the domain properly will make an imprint on your codebase and product.
How creating boundaries around clusters of models will make your code easier to understand, manage, and interact with.
How immutable objects eliminate unnecessary complexities.
How data store access expresses the domain's intentions.
How to represent natural business transactions between models.

7ac2a66004880abc5032b1894bb12fda?s=128

Mike AbiEzzi

September 25, 2014
Tweet

Transcript

  1. 3.
  2. 10.
  3. 11.
  4. 12.
  5. 13.
  6. 14.
  7. 16.

    AIM OF THIS TALK ✘ Custom DDD architectures! ✘ DDD

    design patterns! ✘ Advanced DDD topics (e.g. Repository Pattern) (e.g. Bounded Context)
  8. 17.

    AIM OF THIS TALK ✘ Custom DDD architectures! ✘ DDD

    design patterns! ✘ Advanced DDD topics (e.g. Repository Pattern) (e.g. Bounded Context) custom is expensive!
  9. 19.

    AIM OF THIS TALK ✓ DDD principles ✓ DDD+Rails w/o

    a heavy investment we want to hit the ground running!
  10. 21.

    1. Defining the domain! 2. Communicating the domain! 3. Relationships

    between models! 4. Aggregates! 5. Data access! 6. Value Objects! 7. Domain Services
  11. 23.
  12. 28.
  13. 29.
  14. 30.

    Developer 1 n App Store Customer App n n Purchase

    Version 1 n 1..6 Screenshot 1 brainstorm
  15. 31.

    Developer 1 n App Store Customer App Install n n

    n n Purchase Version 1 n 1..6 Screenshot 1 brainstorm
  16. 32.

    Developer 1 n App Store Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 brainstorm
  17. 33.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 refine
  18. 34.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Developer/ Company refine
  19. 35.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Developer/ Company Seller refine
  20. 36.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller refine
  21. 37.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller refine
  22. 42.

    COMMUNICATION Domain! Expert Software! Expert brainstorm → draw diagrams →

    ! speak out assumptions → let them correct you → refine
  23. 44.

    RESPONSIBILITIES “Domain experts should object to terms or structures that

    are awkward or inadequate to convey domain understanding” -- Eric Evans
  24. 45.

    RESPONSIBILITIES “Domain experts should object to terms or structures that

    are awkward or inadequate to convey domain understanding” “Developers should watch for ambiguity or inconsistency that will trip up design.” -- Eric Evans
  25. 46.

    UBIQUITOUS LANGUAGE “a common, rigorous language between developers and users

    […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  26. 47.

    UBIQUITOUS LANGUAGE User “a common, rigorous language between developers and

    users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  27. 48.

    UBIQUITOUS LANGUAGE Product! Owner User “a common, rigorous language between

    developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  28. 49.

    UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User “a common, rigorous

    language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  29. 50.

    Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User “a common,

    rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  30. 51.

    Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User Designer “a

    common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  31. 52.

    Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User Designer

    “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  32. 53.

    Code Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User

    Designer “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler
  33. 54.

    Code Developer Tester UBIQUITOUS LANGUAGE Domain! Expert Product! Owner User

    Designer “a common, rigorous language between developers and users […] the need for it to be rigorous, since software doesn't cope well with ambiguity” — Martin Fowler Ensure one consistent language
  34. 56.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  35. 57.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end relationships
  36. 58.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end ✓ relationships
  37. 59.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller class App < ActiveRecord::Base has_many :customers, through: :purchases ... end ! ! class Customer < ActiveRecord::Base has_many :apps, through: :purchases ... end ✓ ✘ relationships
  38. 60.

    App Store Developer 1 n Customer App Install n n

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  39. 61.

    1 n App Store Developer 1 n Customer App Install

    n n Purchase Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  40. 62.

    1 n App Store Developer 1 n Customer App Purchase

    Comment 1 n n 1 Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  41. 63.

    1 n App Store Developer 1 n Customer App Purchase

    Comment 1 n Review! comment! rating Version 1 n 1..6 Screenshot 1 Release! version_number Developer/ Company Seller relationships
  42. 66.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  43. 67.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  44. 68.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  45. 69.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  46. 70.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release Submit a new release of an app
  47. 71.

    release = Release.new( version_major: 1, version_minor: 1, ...) release.screenshots <<

    [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Submit a new release of an app
  48. 72.

    app.submit_release(“1.1.0”, screenshots: ["shot1.png", "shot2.png"]) release = Release.new( version_major: 1, version_minor:

    1, ...) release.screenshots << [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Submit a new release of an app
  49. 73.

    app.submit_release(“1.1.0”, screenshots: ["shot1.png", "shot2.png"]) release = Release.new( version_major: 1, version_minor:

    1, ...) release.screenshots << [ Screenshot.new(file: "shot1.png"), Screenshot.new(file: "shot2.png")] release.status = :submitted ! app.releases << release A BETTER WAY? Describe domain behaviors with methods Submit a new release of an app
  50. 74.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 AGGREGATE Release! version_number
  51. 75.

    class App < ActiveRecord::Base ... ! def submit_release ... def

    approve_release ... def flag_for_abuse ... ! def mark_as_staff_favorite ... ! end
  52. 76.

    class App < ActiveRecord::Base ... ! def submit_release ... def

    approve_release ... def flag_for_abuse ... ! def mark_as_staff_favorite ... ! end Tells a story of how the domain works
  53. 80.

    Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  54. 81.

    Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  55. 82.

    Data Access App.where( "create_at > ? and purchase_count > ?",

    1.week.ago, 10000).all require 'fig_leaf' ! class App < ActiveRecord::Base scope :new_and_noteworthy, -> { where("create_at > ? and purchases > ?", 1.week.ago, 10000) } end Use scopes
  56. 83.

    class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end
  57. 84.

    class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end One expressive point of entry
  58. 85.

    class App < ActiveRecord::Base scope :new_and_noteworthy, ... scope :staff_picks, ...

    scope :most_popular, ... ... ! def submit_release ... def approve_release ... ... end One expressive point of entry Aggregate roots are the domain’s only ! point of entry for data access.
  59. 86.
  60. 87.

    ▾ app/! ▾ models/! ▾ apps/! app.rb! release.rb! review.rb! screenshot.rb!

    version_number.rb! ▸ customers/! ▸ sellers/ Aggregate Aggregate Aggregate
  61. 93.
  62. 94.

    ENTITY VALUE OBJECT a thing describes a thing class Customer

    class Name Me
 “Mike AbiEzzi” My little cousin
 “Mike AbiEzzi”
  63. 95.
  64. 96.

    ENTITY VALUE OBJECT a thing describes a thing unique independent!

    of attributes has a lifecycle class Customer class Name
  65. 97.

    ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes has a lifecycle class Customer class Name
  66. 98.

    ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes has a lifecycle class Customer class Name “Mike AbiEzzi” “Mike AbiEzzi”
  67. 99.

    ENTITY VALUE OBJECT a thing describes a thing can change

    state unique independent! of attributes immutable has a lifecycle class Customer class Name
  68. 100.

    ENTITY VALUE OBJECT a thing describes a thing can change

    state doesn’t reference anything unique independent! of attributes immutable has a lifecycle class Customer class Name
  69. 101.

    ENTITY VALUE OBJECT a thing describes a thing can change

    state doesn’t reference anything unique independent! of attributes avoids design complexities immutable has a lifecycle class Customer class Name
  70. 102.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Release! version_number What’s what?
  71. 103.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Entity Release! version_number What’s what?
  72. 104.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Entity Release! version_number Entity What’s what?
  73. 105.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what?
  74. 106.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what? ?
  75. 107.

    App 1 Comment 1 n Review! comment! rating n 1..6

    Screenshot 1 Value Entity Release! version_number Entity What’s what? ? Value
  76. 108.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  77. 109.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  78. 110.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  79. 111.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  80. 112.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  81. 113.

    class Screenshot attr_reader :file, :position ! def initialize(file, position) @file,

    @position = file, position end ! def ==(other) file == other.file && position == other.position end alias_method :eql?, :== def hash; file.hash ^ position.hash; end end VALUE OBJECT immutable / equality
  82. 114.

    class VersionNumber attr_reader :major, :minor, :build ... def next_major_version new

    VersionNumber( major + 1, minor, build) end ! def next_minor_version ... def next_build_version ... end VALUE OBJECT factory methods
  83. 115.

    class VersionNumber attr_reader :major, :minor, :build ... def next_major_version new

    VersionNumber( major + 1, minor, build) end ! def next_minor_version ... def next_build_version ... end VALUE OBJECT factory methods
  84. 118.

    3 ways to persist value objects A. Inline on the

    Entity’s table B. Serialized on the Entity’s table
  85. 119.

    3 ways to persist value objects A. Inline on the

    Entity’s table B. Serialized on the Entity’s table C. In its own table
  86. 120.

    Release! VersionNumber! Entity Value 1 1 A. Inline on the

    Entity’s table releases version_major version_minor version_build …
  87. 121.

    A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  88. 122.

    A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  89. 123.

    A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  90. 124.

    A. Inline on the Entity’s table class Release < ActiveRecord::Base

    def version_number=(vn) @version_number = vn self[:version_major] = vn.major self[:version_minor] = vn.minor self[:version_build] = vn.build end ! def version_number @version_number ||= new VersionNumber( self[:version_major], ... ) end ! private attr_accessor :version_major, ... ! end
  91. 126.

    require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  92. 127.

    require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  93. 128.

    require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  94. 129.

    require 'oj' ! class Release < ActiveRecord::Base ! def screenshots=(screenshots)

    @screenshots = screenshots self[:screenshots] = Oj.dump(screenshots) end ! def screenshots @screenshots ||= Oj.load(self[:screenshots]) end ! end B. Serialized on the Entity’s table
  95. 130.
  96. 131.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  97. 132.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  98. 133.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  99. 134.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  100. 135.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  101. 136.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  102. 137.

    C. In its own table class Review < ActiveRecord::Base after_save

    { |record| immutable!(record) } after_find { |record| immutable!(record) } ! def immutable!(record) record.readonly! attributes.each do |attr, _| record.define_singleton_method :"#{attr}=" do |_| raise "readonly" end end end ! def ==(other) ... alias_method :eql?, :== def hash ... end
  103. 141.

    Gift an App SERVICE 1. Charge the gifter that’s purchasing

    the app. 2. Assign access rights to the giftee receiving the app.
  104. 142.

    Gift an App SERVICE 1. Charge the gifter that’s purchasing

    the app. 2. Assign access rights to the giftee receiving the app. Facilitates a transaction between two aggregates
  105. 145.

    class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  106. 146.

    class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  107. 147.

    class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  108. 148.

    class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  109. 149.

    class GiftApp def self.execute(app, gifter, giftee) Purchase.create( app: app, customer:

    gifter, ...) AccessRight.create( app: app, customer: giftee, purchase: purchase, ...) end end Purchase 1 n Customer AccessRight 1 n
  110. 151.
  111. 160.

    ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ...
  112. 161.

    ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates
  113. 162.

    ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates entry point to domain behaviors & ! data retrieval
  114. 163.

    ▾ app/! ▾ models/! ▸ apps/! app.rb! ...! ▸ customers/!

    ▸ sellers/! ▸ ...! ▸ services/! gift_app.rb! refund_purchase.rb! ... aggregates cross aggregate transactions entry point to domain behaviors & ! data retrieval