agentleFS
dilolabsdilolabs/nosia/.claude/skills/model-patterns/SKILL.md

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).

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
  1. Very specific:

    Always: Put business logic in models, use concerns for organization, use bang methods (create!, update!), leverage associations and scopes, …

    line 294
  2. Distinctive:

    Ask first: Before creating service objects, before adding complex callbacks, before using inheritance (prefer composition via concerns)

    line 295
  3. Distinctive:

    Never: Create anemic models (data without behavior), put business logic in controllers, skip validations, create models without tests

    line 296

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 console

Rich 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.close

Model 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
end

Association 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_at

has_many / has_one

has_many :comments, dependent: :destroy
has_many :assignees, through: :assignments, source: :assignee
has_one :closure, dependent: :destroy

Polymorphic

has_many :attachments, as: :attachable, dependent: :destroy
has_many :events, as: :eventable, dependent: :destroy
belongs_to :notifiable, polymorphic: true

Counter caches

belongs_to :card, counter_cache: :comments_count
belongs_to :board, counter_cache: :cards_count

Scope 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?           # Conditional

Callbacks 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: true

Business 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 }
end

Query 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
end

Computed 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_later

Using 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
end

See 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

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.