Code Reading - Learn ❤️ Domain Driven Design
Notes from Flatiron School engineering team’s code reading on applying domain driven design principles to our legacy Rails codebase.
Introducing DDD into Existing Codebase
Risky to experiment with existing “domains”. Better to experiment with something new.
Our test case: “Compliance” and “Notifications” domains
- wouldn’t affect existing code
Didn’t add any new
has_many or other associations
Communicate with domain through top level
domains/:domain modules (ex.
Gives us bounded context. Send messages to bounded contexts, not the subdomains directly. All requests should be funneled through top level module.
That said, we’re not being strict about this convention. You still can call models directly. We may change this in the future to be stricter, but we’re trusting our team to self police for now.
# domains/compliance.rb module Compliance def self.get_requirements_with_document(requirement_ids) Requirement.includes(:document).find(requirement_ids) end def self.requirement_complete?(requirement_id) Requirement.complete.where(id: requirement_id).exists? end # etc... end # models/requirement.rb module Compliance class Requirement < ActiveRecord::Base end end
Fun Rails syntax note: nested syntax is interpretted differently than
:: syntax (as we learned in this helpful post).
module Compliance class Requirement < ActiveRecord::Base end end # *** # does not equal # *** class Compliance::Requirement < ActiveRecord::Base end # If you use the latter syntax, you'll need to prefix references to your model in your top level domain methods: module Compliance def self.get_requirements_with_document(requirement_ids) self::Requirement.includes(:document).find(requirement_ids) end end
DDD provides a clean, small interface where it’s much easier to swap underlying things out. Helpful for keeping code decoupled and easier to build on or deprecate.
The Benefits of micro-services, but all in one easy-to-ack codebase.
We overrode Rails conventions for table names, for ease of reading.
module Notifications class Compliance < ActiveRecord::Base # By default, Rails would have named this table `compliance`, # which is too general for how we're using this self.table_name = "compliance_notifications" end end
Associating other models in the domain also requires us to be more explicit than usual.
module Compliance class Requirement < ActiveRecord::Base self.table_name = "compliance_requirements" belongs_to :document, class_name: Compliance::Document, foreign_key: "compliance_document_id" end end
Controllers talk to the domain only, not underlying models.
module Compliance class RequirementsController < ApplicationController def show requirement_id = params[:id] if Compliance.requirement_complete?(requirement_id) envelope_id = Compliance.get_external_reference_id(requirement_id) pdf = Compliance.get_signed_document(envelope_id: envelope_id) send_data pdf, type: 'application/pdf' else requirement_url = Compliance.get_external_url_for_requirement(requirement_id, student: current_user) redirect_to requirement_url end end end end