Tips and Tricks¶
Patterns and conventions for building maintainable CMDx applications.
Project Organization¶
Directory Structure¶
A predictable layout keeps tasks discoverable as a project grows:
/app/
└── /tasks/
├── /invoices/
│ ├── calculate_tax.rb
│ ├── validate_invoice.rb
│ ├── send_invoice.rb
│ └── process_invoice.rb # workflow
├── /reports/
│ ├── generate_pdf.rb
│ ├── compile_data.rb
│ ├── export_csv.rb
│ └── create_reports.rb # workflow
├── application_task.rb # base class
├── authenticate_session.rb
└── activate_account.rb
Naming Conventions¶
Follow consistent naming patterns for clarity:
# Verb + Noun
class ExportData < CMDx::Task; end
class CompressFile < CMDx::Task; end
class ValidateSchema < CMDx::Task; end
# Use present-tense verbs for actions
class GenerateToken < CMDx::Task; end # ✓ Good
class GeneratingToken < CMDx::Task; end # ❌ Avoid
class TokenGeneration < CMDx::Task; end # ❌ Avoid
Story Telling¶
Break complex logic into descriptively named methods so work reads like a narrative:
class ProcessOrder < CMDx::Task
def work
charge_payment_method
assign_to_warehouse
send_notification
end
private
def charge_payment_method
order.primary_payment_method.charge!
end
def assign_to_warehouse
order.ready_for_shipping!
end
def send_notification
if order.products_out_of_stock?
OrderMailer.pending(order).deliver
else
OrderMailer.preparing(order).deliver
end
end
end
Style Guide¶
Follow this order for consistent, readable tasks:
class ExportReport < CMDx::Task
# 1. Settings, retries, deprecation
settings tags: [:reporting]
retry_on Net::ReadTimeout, limit: 3, jitter: :exponential
# 2. Register custom extensions
register :middleware, Telemetry::Middleware.new
register :validator, :phone, PhoneValidator
# 3. Define callbacks
before_execution :find_report
on_complete :track_export_metrics, if: -> { Current.tenant.analytics? }
# 4. Declare inputs
optional :user_id
required :report_id, coerce: :integer
optional :format_type, coerce: :string, inclusion: { in: %w[pdf csv] }
# 5. Declare outputs (the contract)
output :exported_at
# 6. Define work
def work
report.compile!
report.export!
context.exported_at = Time.now
end
# TIP: Favor private business logic to reduce the surface of the public API.
private
# 7. Helpers
def find_report
@report ||= Report.find(report_id)
end
def track_export_metrics
Analytics.increment(:report_exported)
end
end
Sharing Input Options¶
Use with_options to factor out repeated options across input declarations.
Note
with_options is provided by ActiveSupport and is available automatically in Rails. For plain Ruby projects, add require "active_support/core_ext/object/with_options" or apply shared options manually.
class ConfigureCompany < CMDx::Task
with_options(coerce: :string, presence: true) do
optional :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
required :company_name, :industry
optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
end
required :headquarters do
with_options(coerce: :string) do
optional :street, :city, :zip_code
required :country, inclusion: { in: VALID_COUNTRIES }
optional :region
end
end
def work
# ...
end
end
with_options works inside nested-input blocks too because the child DSL is evaluated with instance_eval.
Sharing Behavior via a Base Class¶
Pull cross-cutting concerns onto a base task. Subclasses inherit settings, callbacks, middlewares, coercions, validators, executors, mergers, retriers, deprecators, telemetry, inputs, outputs, retry_on, and deprecation automatically.
class ApplicationTask < CMDx::Task
settings tags: [:app]
retry_on Net::OpenTimeout, Net::ReadTimeout, limit: 2
before_execution :ensure_current_tenant!
private
def ensure_current_tenant!
fail!("missing tenant") if Current.tenant.nil?
end
end
class ProcessInvoice < ApplicationTask
required :invoice_id, coerce: :integer
def work
# Inherits settings, retry_on, and the before_execution callback
end
end
Inherited registries (callbacks, middlewares, validators, coercions, executors, mergers, retriers, deprecators, inputs, outputs) accumulate — declaring more in a subclass appends to (or overwrites by name in) the parent's list. To opt out of an inherited entry, use deregister (e.g. deregister :callback, :before_execution, :ensure_current_tenant!). retry_on and settings likewise accumulate via merge: a subclass retry_on adds exception classes and overrides individual options (limit:, delay:, …) without dropping the parent's, and settings merges new keys on top.
Useful Examples¶
- Active Job Durability
- Active Record Database Transaction
- Active Record Query Tagging
- Active Support Instrumentation
- dry-monads Interop
- Flipper Feature Flags
- GraphQL Resolvers
- OpenAPI Schema Generation
- OpenTelemetry Tracing
- Paper Trail Whatdunnit
- PubSub Task Chaining
- Pundit Authorization
- Rate Limit
- Redis Idempotency
- Sentry Error Tracking
- Sidekiq Async Execution
- Stoplight Circuit Breaker
- Timeout Guard