| name | ruby-patterns-advanced |
| description | Advanced Ruby patterns โ DDD with domain objects, Sorbet type system, value objects, event sourcing, background jobs, and API response standards with RFC 7807. |
Ruby Patterns Advanced
Advanced Ruby on Rails patterns for production-grade applications covering DDD, type safety, event-driven design, and API standards.
When to Activate
- Implementing Domain-Driven Design in Rails (aggregates, value objects, domain events)
- Adding Sorbet type annotations to a Ruby codebase
- Designing event sourcing or CQRS patterns in Rails
- Building background job pipelines with Sidekiq
- Implementing RFC 7807 Problem Details error responses
- Refactoring to clean hexagonal architecture in Rails
Domain-Driven Design in Rails
Value Objects
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency)
raise ArgumentError, "amount must be non-negative" if amount < 0
raise ArgumentError, "invalid currency" unless %w[USD EUR GBP].include?(currency)
@amount = amount.freeze
@currency = currency.freeze
freeze
end
def +(other)
raise ArgumentError, "currency mismatch" unless currency == other.currency
Money.new(amount + other.amount, currency)
end
def <=>(other)
return nil unless currency == other.currency
amount <=> other.amount
end
def to_s = "#{currency} #{format('%.2f', amount)}"
def zero? = amount.zero?
end
Aggregate Root
class Order
attr_reader :id, :status, :line_items, :events
def initialize(id:)
@id = id
@status = :draft
@line_items = []
@events = []
end
def add_item(product_id:, quantity:, price:)
raise DomainError, "Cannot modify confirmed order" unless draft?
raise ArgumentError, "quantity must be positive" unless quantity.positive?
@line_items << LineItem.new(product_id: product_id, quantity: quantity, price: price)
@events << OrderItemAdded.new(order_id: id, product_id: product_id)
self
end
def confirm
raise DomainError, "Cannot confirm empty order" if line_items.empty?
raise DomainError, draft?
=
<< .new( id, total)
line_items.sum { || item.price * item.quantity }
= status ==
= status ==
Domain Events
OrderConfirmed = Data.define(:order_id, :total, :occurred_at) do
def initialize(order_id:, total:, occurred_at: Time.current)
super
end
end
class OrderEventPublisher
def publish(events)
events.each do |event|
case event
when OrderConfirmed
OrderMailer.confirmation(event.order_id).deliver_later
Analytics.track("order.confirmed", order_id: event.order_id, total: event.total)
when OrderItemAdded
InventoryService.reserve(event.product_id)
end
end
end
end
Sorbet Type System
Basic Typed Module
class PaymentService
extend T::Sig
sig { params(order: Order, card_token: String).returns(T::Boolean) }
def charge(order, card_token)
response = Stripe::Charge.create(
amount: (order.total * 100).to_i,
currency: order.currency,
source: card_token
)
response.status == "succeeded"
rescue Stripe::CardError => e
Rails.logger.error("Payment failed: #{e.message}")
false
end
end
Typed Interfaces (Abstract Classes)
module PaymentGateway
extend T::Helpers
extend T::Sig
interface!
sig { abstract.params(amount_cents: Integer, token: String).returns(T::Boolean) }
def charge(amount_cents, token); end
sig { abstract.params(charge_id: String).returns(T::Boolean) }
def refund(charge_id); end
end
class StripeGateway
include PaymentGateway
extend T::Sig
sig { override.params(amount_cents: Integer, token: String).returns(T::Boolean) }
def charge(amount_cents, token)
end
end
RFC 7807 Problem Details
FastAPI/Rails API error standard โ use for all 4xx/5xx responses:
module ProblemDetails
extend ActiveSupport::Concern
included do
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
rescue_from DomainError, with: :unprocessable_entity
end
private
def render_problem(status:, title:, detail:, type: nil, **extras)
render(
json: {
type: type || "about:blank",
title: title,
status: status,
detail: detail,
instance: request.path,
**extras
}.compact,
status: status,
content_type: "application/problem+json"
)
end
def not_found(exception)
render_problem(
status: 404,
title: "Not Found",
detail: exception.message
)
end
()
render_problem(
,
,
exception.message
)
()
render_problem(
,
,
)
<
Advanced Background Jobs
Idempotent Sidekiq Jobs
class ProcessOrderJob < ApplicationJob
queue_as :critical
sidekiq_options retry: 3, dead: false
def perform(order_id)
order = Order.find(order_id)
return if order.processed?
ActiveRecord::Base.transaction do
order.process!
order.update!(processed_at: Time.current)
end
OrderEventPublisher.new.publish(order.events)
rescue ActiveRecord::RecordNotFound
Rails.logger.warn("Order #{order_id} not found โ skipping job")
end
end
Job Batching with Sidekiq Pro
class ImportProductsJob < ApplicationJob
def perform(file_url)
batch = Sidekiq::Batch.new
batch.description = "Product import #{file_url}"
batch.on(:complete, ImportCompleteCallback, file_url: file_url)
batch.jobs do
CSV.parse(URI.open(file_url).read, headers: true).each_slice(100) do |rows|
ImportProductBatchJob.perform_later(rows.map(&:to_h))
end
end
end
end
class ImportCompleteCallback
def on_complete(status, options)
Rails.logger.info("Import complete: #{status.total} products from #{options['file_url']}")
AdminMailer.import_complete(status.total).deliver_later
end
end
Query Objects
class ActiveOrdersQuery
def initialize(relation = Order.all)
@relation = relation
end
def call(user:, status: nil, limit: 20)
result = @relation
.where(user_id: user.id)
.includes(:line_items, :shipping_address)
.order(created_at: :desc)
.limit(limit)
result = result.where(status: status) if status
result
end
end
orders = ActiveOrdersQuery.new.call(user: current_user, status: :confirmed)
Related Skills
ruby-patterns โ Core patterns: Service Objects, Form Objects, Decorators
ruby-testing โ RSpec, FactoryBot, shared examples, request specs
ddd-python โ DDD concepts applicable across languages