Code Reading - Learn ❤️ Domain Driven Design

Published Thursday, August 10, 2017

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

  • small
  • isolated
  • wouldn’t affect existing code

Test Case

Didn’t add any new has_many or other associations

Domains

Communicate with domain through top level domains/:domain modules (ex. domains/compliance.rb).

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.

Models

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

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

Additional Resources

  1. Starr Horne - “Avoid These Traps When Nesting Ruby Modules”