Use when writing Rails models - enforces state-as-records not booleans, concerns as adjectives namespaced under model, and concern extraction triggers
From vanilla-railsnpx claudepluginhub zemptime/zemptime-marketplace --plugin vanilla-railsThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Rich domain models with concerns. Decompose with concerns, not services.
For method ordering and private indentation rules, see vanilla-rails-style.
Don't use boolean columns for state. Create state records that capture who/when.
# Bad - boolean
add_column :cards, :starred, :boolean, default: false
# Good - state record
create_table :stars, id: :uuid do |t|
t.uuid :card_id, null: false
t.uuid :user_id, null: false
t.timestamps
end
class Card < ApplicationRecord
has_one :star, dependent: :destroy
def star(user: Current.user)
create_star!(user: user) unless starred?
end
def starred?
star.present?
end
end
Binary state (one per item): has_one — closure, triage, goldness
Multi-user state: has_many — pins, watches, assignments
Name as adjectives (-able/-ible ONLY). Namespace under the model. File at app/models/card/closeable.rb.
| Wrong | Right | Why |
|---|---|---|
Card::Closing | Card::Closeable | Verb → adjective |
Card::Stars | Card::Starrable | Noun → adjective |
Card::Closed | Card::Closeable | Past participle → capability |
Starrable | Card::Starrable | Must namespace under model |
concerns/starrable.rb | card/starrable.rb | File under model directory |
Full pattern:
# app/models/card/closeable.rb
module Card::Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
end
def close(user: Current.user)
unless closed?
transaction do
create_closure!(user: user)
track_event :closed, creator: user
end
end
end
def reopen(user: Current.user)
if closed?
transaction do
closure&.destroy
track_event :reopened, creator: user
end
end
end
def closed?
closure.present?
end
end
Multi-user pattern:
module Card::Pinnable
extend ActiveSupport::Concern
included do
has_many :pins, dependent: :destroy
end
def pinned_by?(user)
pins.exists?(user: user)
end
def pin_by(user)
pins.find_or_create_by!(user: user)
end
def unpin_by(user)
pins.find_by(user: user)&.destroy
end
end
| Red flag | Fix |
|---|---|
| Boolean column for state | State record table (see data-modeling) |
| Concern not ending in -able/-ible | Rename immediately |
| Concern not namespaced under model | Move to Card::Closeable |
Concern in app/models/concerns/ for single model | Move to app/models/card/ |
| Service object for domain logic | Rich model method instead |
| "Only extract if reused" | Extract for decomposition at 3+ methods |