model-patterns
Skill211 starsChanged 5 months agoMITView on GitHub
Builds rich domain models with business logic, concerns, and proper associations following the fat-models-over-service-objects philosophy. Use when creating models, adding validations, scopes, callbacks, business logic methods, or associations. WHEN NOT: Controller/routing work (use crud-patterns), concern extraction (use concern-patterns), state record design (use state-records).
In this repository17 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- .opencode/CLAUDE.mdCLAUDE.md
- api-patternsSkill
- auth-setupSkill
- caching-patternsSkill
- concern-patternsSkill
- crud-patternsSkill
- event-trackingSkill
- job-patternsSkill
- mailer-patternsSkill
- migration-patternsSkill
- multi-tenant-setupSkill
- state-recordsSkill
- stimulus-patternsSkill
- testing-patternsSkill
- turbo-patternsSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 294
Always: Put business logic in models, use concerns for organization, use bang methods (create!, update!), leverage associations and scopes, …
- Distinctive: line 295
Ask first: Before creating service objects, before adding complex callbacks, before using inheritance (prefer composition via concerns)
- Distinctive: line 296
Never: Create anemic models (data without behavior), put business logic in controllers, skip validations, create models without tests
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Model Patterns (37signals)
Rich domain models over service objects. Business logic lives in models, not in separate service classes.
Project knowledge
Tech Stack: Rails 8.2 (edge), UUIDs everywhere, database-backed everything (no Redis) Patterns: Heavy use of concerns, default values via lambdas, Current for context
Commands:
bin/rails generate model Card title:string body:text board:references:uuid
bin/rails generate migration AddColorToCards color:string
bin/rails db:migrate
bin/rails test test/models/
bin/rails consoleRich model vs service object
# BAD -- service object
class CloseCardService
def initialize(card, user)
@card = card
@user = user
end
def call
ActiveRecord::Base.transaction do
@card.create_closure!(user: @user)
@card.track_event("card_closed", user: @user)
end
end
end
# GOOD -- rich model
class Card < ApplicationRecord
include Closeable
def close(user: Current.user)
create_closure!(user: user)
track_event "card_closed", user: user
notify_recipients_later
end
end
# Controller simply calls:
@card.closeModel structure
Order within a model:
class Card < ApplicationRecord
# 1. Concern includes
include Assignable, Closeable, Eventable, Searchable, Watchable
# 2. Associations
belongs_to :account, default: -> { board.account }
belongs_to :board, touch: true
belongs_to :column, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_many :comments, dependent: :destroy
has_many :assignments, dependent: :destroy
has_one :closure, dependent: :destroy
# 3. Validations
validates :title, presence: true
validates :status, inclusion: { in: %w[draft published archived] }
# 4. Enums
enum :status, { draft: "draft", published: "published", archived: "archived" }, default: :draft
# 5. Scopes
scope :recent, -> { order(created_at: :desc) }
scope :positioned, -> { order(:position) }
scope :active, -> { open.published.where.missing(:not_now) }
# 6. Delegations
delegate :name, to: :board, prefix: true, allow_nil: true
# 7. Callbacks (sparingly)
after_create_commit :broadcast_creation
# 8. Business logic methods
def publish
update!(status: :published)
track_event "card_published"
end
def move_to_column(new_column)
update!(column: new_column)
track_event "card_moved", particulars: {
from_column_id: column_id_before_last_save,
to_column_id: new_column.id
}
end
private
def broadcast_creation
broadcast_prepend_to board, :cards, target: "cards", partial: "cards/card"
end
endAssociation patterns
belongs_to with defaults
belongs_to :account, default: -> { board.account }
belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :board, touch: true # Updates parent's updated_athas_many / has_one
has_many :comments, dependent: :destroy
has_many :assignees, through: :assignments, source: :assignee
has_one :closure, dependent: :destroyPolymorphic
has_many :attachments, as: :attachable, dependent: :destroy
has_many :events, as: :eventable, dependent: :destroy
belongs_to :notifiable, polymorphic: trueCounter caches
belongs_to :card, counter_cache: :comments_count
belongs_to :board, counter_cache: :cards_countScope patterns
# Basic ordering
scope :recent, -> { order(created_at: :desc) }
scope :positioned, -> { order(:position) }
# With arguments
scope :by_creator, ->(user) { where(creator: user) }
scope :created_after, ->(date) { where("created_at > ?", date) }
# Joins and where.missing (key pattern for state records)
scope :assigned_to, ->(users) { joins(:assignments).where(assignments: { assignee: users }).distinct }
scope :open, -> { where.missing(:closure) }
scope :unassigned, -> { where.missing(:assignments) }
# Complex composed scopes
scope :entropic, -> {
open.published.where.missing(:not_now).where("updated_at < ?", 30.days.ago)
}Validation patterns
validates :title, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :email_address, uniqueness: { case_sensitive: false }
validates :user_id, uniqueness: { scope: :card_id } # Join tables
validates :card, uniqueness: true # has_one state records
validates :body, presence: true, if: :published? # ConditionalCallbacks and enums
# Callbacks: use sparingly, prefer _commit for external effects
after_create_commit :broadcast_creation
before_validation :set_default_status, on: :create
after_create_commit :notify_recipients_later # Uses _later convention
# String enums (preferred for DB readability)
enum :status, {
draft: "draft", published: "published", archived: "archived"
}, default: :draft, prefix: trueBusiness logic methods
Action methods (verbs)
def close(user: Current.user)
create_closure!(user: user)
track_event "card_closed", user: user
notify_watchers_later
end
def assign(user)
assignments.create!(user: user) unless assigned_to?(user)
track_event "card_assigned", particulars: { assignee_id: user.id }
endQuery methods (predicates)
def closed?
closure.present?
end
def assigned_to?(user)
assignees.include?(user)
end
def can_be_edited_by?(user)
user.can_administer_card?(self) || creator == user
endComputed attributes
def closed_at
closure&.created_at
end
def closed_by
closure&.user
end_later / _now convention
# Async version (queues a job)
def notify_recipients_later
NotifyRecipientsJob.perform_later(self)
end
# Sync version (immediate execution)
def notify_recipients_now
recipients.each do |recipient|
Notification.create!(recipient: recipient, notifiable: self)
end
end
# Default to sync
def notify_recipients
notify_recipients_now
end
# Call _later from callbacks
after_create_commit :notify_recipients_laterUsing Current for context
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :identity, :account
end
class Card < ApplicationRecord
belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :account, default: -> { Current.account }
def close(user: Current.user)
create_closure!(user: user)
end
endSee references/model-examples.md for complete model examples (join tables, form objects, POROs, migrations, tests).
Boundaries
- Always: Put business logic in models, use concerns for organization, use bang methods (
create!,update!), leverage associations and scopes, use Current for context, default values via lambdas - Ask first: Before creating service objects, before adding complex callbacks, before using inheritance (prefer composition via concerns)
- Never: Create anemic models (data without behavior), put business logic in controllers, skip validations, create models without tests
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

