Domain‐Driven Design (DDD) Best Practices - fast-programmer/outboxer GitHub Wiki
Domain-Driven Design (DDD) Best Practices
Introduction
This guide offers concise, opinionated best practices for applying Domain-Driven Design (DDD) in Ruby applications, using the Accountify::InvoiceService.issue
method as a case study.
1. Model the Ubiquitous Language
Use the same language in code that you use with domain experts. Invoices are “issued,” not “created” or “saved.”
status: Invoice::Status::ISSUED
Avoid:
status: "created" # vague and not domain-aligned
2. Use Value Objects for Precision
Use Money
instead of Integer
or Float
to prevent rounding errors and increase clarity.
total_amount: Money.new(25000, "AUD")
Avoid:
total_amount: 250.00 # prone to floating point errors
3. Application Services as Aggregate Roots
Application services (e.g., InvoiceService
) can act as aggregate roots at the service level by applying model changes and creating an event.
class InvoiceService
def self.issue(...)
...
end
end
This structure is useful when the aggregate logic spans multiple models or bounded contexts.
4. Keep Application Services Stateless
Use stateless services that coordinate domain logic, but do not hold state themselves.
Accountify::InvoiceService.issue(...)
5. Use Domain Events to Capture Facts
Create explicit domain events to document and react to domain changes.
event = InvoiceIssuedEvent.create!(...)
Benefits:
- Enables event-driven architecture
- Encourages decoupled systems
6. Use the Outbox Pattern for Reliability
Queue events in the same transaction as the state change.
Outboxer::Message.queue(messageable: event, queued_at: current_time)
Why:
- Guarantees delivery of the event if the transaction commits
7. Explicit Time Handling
Always pass Time
explicitly to ensure testability and deterministic behavior.
time: Time.current
8. Use Hash Returns for Simple Service Output
Return a hash with symbol keys for predictable service outcomes.
{ id: invoice.id, event_id: event.id }
9. Transactions Should Encapsulate All Writes
Use ActiveRecord::Base.transaction
to ensure atomicity of the operation.
ActiveRecord::Base.transaction do
...
end
10. Avoid Premature Abstractions
Keep logic explicit and close to the domain until duplication or complexity forces a refactor.
Summary Checklist
- Use Ubiquitous Language
- Favor Value Objects
- Use Application Services as Aggregate Roots
- Keep Services Stateless
- Emit Domain Events
- Use Outbox Pattern
- Pass Time Explicitly
- Return Hashes
- Wrap Writes in Transactions
- Avoid Premature Abstraction
Final Thoughts
This guide exemplifies how applying DDD in Ruby can lead to more expressive, robust, and maintainable code. Let application services coordinate your domain logic and events, keep naming idiomatic, and align code structure with domain behavior.
Full Example
# app/models/invoice.rb
class Invoice < ApplicationRecord
module Status
ISSUED = "issued"
DRAFT = "draft"
VOIDED = "voided"
end
end
# app/models/invoice_issued_event.rb
class InvoiceIssuedEvent < ApplicationRecord
end
# app/services/accountify/invoice_service.rb
module Accountify
class InvoiceService
# Issues an invoice.
#
# @param total_amount [Money] The full amount of the invoice
# @param time [Time] The Time dependency
# @return [Hash{Symbol => Integer}] A hash with :id and :event_id
def self.issue(total_amount:, time:)
ActiveRecord::Base.connection_pool.with_connection do
ActiveRecord::Base.transaction do
current_time = time
invoice = Invoice.create!(
status: Invoice::Status::ISSUED,
total_amount_fractional: total_amount.fractional,
total_amount_currency_iso_code: total_amount.currency.iso_code,
issued_at: current_time,
created_at: current_time,
updated_at: current_time
)
event = InvoiceIssuedEvent.create!(
body: {
invoice_id: invoice.id,
issued_at: invoice.issued_at.iso8601,
status: invoice.status,
total_amount_cents: invoice.total_amount_fractional,
currency: invoice.total_amount_currency_iso_code,
created_at: current_time
},
created_at: current_time
)
Outboxer::Message.queue(messageable: event, queued_at: current_time)
{ id: invoice.id, event_id: event.id }
end
end
end
end
end