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
- 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