CrackedRuby CrackedRuby

Model-View-Controller (MVC)

Overview

Model-View-Controller (MVC) is an architectural pattern that divides an application into three distinct components, each with specific responsibilities. The Model manages data and business logic, the View handles presentation and user interface rendering, and the Controller processes user input and coordinates interactions between the Model and View. This separation emerged from Smalltalk development in the 1970s at Xerox PARC and has become the dominant pattern for web application architecture.

The pattern addresses the problem of tangled dependencies where user interface code, business logic, and data access intermingle within the same components. Without clear boundaries, applications become difficult to modify, test, and maintain. A change to the display format requires touching business logic code. Adding a new data source forces modifications to presentation code. Testing becomes complex when UI concerns cannot be isolated from data operations.

MVC establishes clear boundaries through separation of concerns. Each component type has a defined role and communicates with other components through well-defined interfaces. The Model operates independently of how data will be displayed. The View renders data without understanding its source or manipulation. The Controller mediates between them without implementing either data logic or presentation details.

The pattern appears extensively in web frameworks. Ruby on Rails implements MVC as its core organizational principle. Django uses a variant called MTV (Model-Template-View). Spring MVC provides the pattern for Java applications. Frontend frameworks like Angular and Ember adopt MVC-inspired architectures. The pattern's widespread adoption stems from its ability to organize complex applications into manageable, testable pieces.

# Basic MVC flow in a Rails application
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])  # Controller retrieves Model
    # Implicitly renders app/views/articles/show.html.erb (View)
  end
end

# app/models/article.rb
class Article < ApplicationRecord
  validates :title, presence: true
  
  def summary
    content.truncate(100)  # Business logic stays in Model
  end
end

# app/views/articles/show.html.erb
<h1><%= @article.title %></h1>
<p><%= @article.summary %></p>

The Model encapsulates data structures, persistence logic, validation rules, and business operations. It represents the application's information and the rules governing access to that information. Models remain independent of user interface concerns and can be tested without rendering views or processing HTTP requests.

The View generates output representations of Model data. It handles formatting, layout, and presentation logic. Views contain minimal code beyond display directives and never directly manipulate data or implement business rules. Multiple Views can present the same Model data in different formats—HTML, JSON, XML, or PDF.

The Controller receives user input, invokes Model operations, and selects appropriate Views for response generation. It translates user actions into Model method calls and determines which View should render the results. Controllers contain coordination logic but avoid implementing business rules or presentation details.

Key Principles

Separation of Concerns forms the foundation of MVC. Each component handles one aspect of the application: data management, user interface, or request coordination. This separation allows developers to modify one component without affecting others. Database schema changes affect only Models. Interface redesigns impact only Views. New user workflows require only Controller modifications. The pattern prevents the ripple effects that occur when these concerns mix.

Unidirectional Data Flow defines how information moves through the system. User actions trigger Controller methods. Controllers query or update Models. Models notify observers of state changes. Views read Model data for rendering. This flow prevents circular dependencies where components depend on each other in complex ways. The Controller initiates actions, Models manage state, and Views reflect that state without initiating changes.

# Unidirectional flow in practice
class OrdersController < ApplicationController
  def create
    # Controller receives input
    @order = Order.new(order_params)
    
    # Controller invokes Model logic
    if @order.save
      # Model validation and persistence succeed
      # Controller selects View (success response)
      redirect_to @order, notice: 'Order created'
    else
      # Model validation fails
      # Controller selects View (error response)
      render :new, status: :unprocessable_entity
    end
  end
end

Model Independence means Models function without knowledge of Views or Controllers. A Model can validate data, calculate values, and persist changes regardless of whether the data comes from a web form, API request, or background job. This independence enables Model reuse across different interfaces and testing Models in isolation.

class Product < ApplicationRecord
  validates :price, numericality: { greater_than: 0 }
  
  def discounted_price(percentage)
    price * (1 - percentage / 100.0)  # Pure business logic
  end
  
  def low_stock?
    quantity < reorder_threshold
  end
end

# Model works identically from web interface, API, or console
product = Product.find(1)
product.discounted_price(10)  # => 89.99
# No knowledge of HTTP, HTML, or user sessions

View Passivity requires Views to act as display templates without implementing logic. Views iterate over collections, format values, and compose layouts, but they never perform calculations, make decisions based on business rules, or alter Model state. This restriction keeps presentation code simple and testable.

Controller as Coordinator positions Controllers as intermediaries that translate HTTP requests into Model operations and select Views for responses. Controllers parse parameters, invoke Model methods, handle success and failure cases, and choose appropriate response formats. They contain workflow logic—the sequence of operations for handling a request—but delegate actual work to Models and Views.

Component Communication follows established protocols. Controllers instantiate Models and make instance variables available to Views. Views read data from these instance variables. Models use callbacks and observers to notify interested parties of state changes. This communication pattern prevents tight coupling while allowing necessary interaction.

class ReportsController < ApplicationController
  def sales
    # Controller coordinates multiple Models
    @date_range = params[:start_date]..params[:end_date]
    @orders = Order.completed.where(created_at: @date_range)
    @revenue = @orders.sum(:total)
    @product_breakdown = OrderItem.group(:product_id)
                                   .sum(:quantity)
    
    # Controller makes data available to View
    # View receives @orders, @revenue, @product_breakdown
    respond_to do |format|
      format.html  # Renders HTML View
      format.json { render json: { revenue: @revenue } }  # JSON View
    end
  end
end

Testability Through Isolation allows each component to be tested independently. Models undergo unit testing without Views or HTTP concerns. Views can be tested with mock data objects. Controllers can be tested with request simulations. This isolation reduces test complexity and execution time.

Multiple View Support enables different representations of the same Model data. A User Model might render as HTML for browsers, JSON for API clients, and XML for legacy systems. The Model remains unchanged while Views adapt output format to client requirements.

Ruby Implementation

Ruby on Rails provides the canonical Ruby implementation of MVC. Rails generates directory structures that enforce the pattern: app/models, app/views, and app/controllers contain respective components. Rails routing maps URLs to Controller actions, and conventions connect Controllers to Views automatically.

Model Implementation in Rails extends ApplicationRecord, which inherits from ActiveRecord::Base. ActiveRecord implements the Active Record pattern, combining data access and business logic in Model classes. Each Model class maps to a database table, and instances represent individual records.

# app/models/user.rb
class User < ApplicationRecord
  # Associations define relationships
  has_many :posts, dependent: :destroy
  has_many :comments
  belongs_to :organization, optional: true
  
  # Validations enforce business rules
  validates :email, presence: true, uniqueness: true
  validates :username, format: { with: /\A[a-zA-Z0-9_]+\z/ }
  validates :age, numericality: { greater_than_or_equal_to: 13 }
  
  # Scopes provide reusable queries
  scope :active, -> { where(deactivated_at: nil) }
  scope :admins, -> { where(role: 'admin') }
  
  # Callbacks execute at lifecycle points
  before_save :normalize_email
  after_create :send_welcome_email
  
  # Business logic methods
  def full_name
    "#{first_name} #{last_name}"
  end
  
  def can_post?
    active? && !banned?
  end
  
  private
  
  def normalize_email
    self.email = email.downcase.strip
  end
  
  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

Controller Implementation extends ApplicationController, which inherits from ActionController::Base. Controllers define action methods corresponding to CRUD operations and custom workflows. Rails routing automatically maps HTTP verbs and URLs to these actions.

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  before_action :authorize_post, only: [:edit, :update, :destroy]
  
  def index
    @posts = Post.published.includes(:author)
                  .page(params[:page])
                  .per(20)
  end
  
  def show
    @comments = @post.comments.approved.includes(:user)
    @related_posts = Post.in_category(@post.category_id)
                         .where.not(id: @post.id)
                         .limit(5)
  end
  
  def new
    @post = current_user.posts.build
  end
  
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      redirect_to @post, notice: 'Post created successfully'
    else
      render :new, status: :unprocessable_entity
    end
  end
  
  def edit
    # @post set by before_action
  end
  
  def update
    if @post.update(post_params)
      redirect_to @post, notice: 'Post updated successfully'
    else
      render :edit, status: :unprocessable_entity
    end
  end
  
  def destroy
    @post.destroy
    redirect_to posts_url, notice: 'Post deleted successfully'
  end
  
  private
  
  def set_post
    @post = Post.find(params[:id])
  end
  
  def authorize_post
    unless @post.user_id == current_user.id || current_user.admin?
      redirect_to root_path, alert: 'Not authorized'
    end
  end
  
  def post_params
    params.require(:post).permit(:title, :content, :category_id, :published)
  end
end

View Implementation uses ERB (Embedded Ruby) templates by default, though Rails supports Haml, Slim, and other template engines. Views reside in directories matching Controller names, with file names matching action names.

<!-- app/views/posts/show.html.erb -->
<article class="post">
  <header>
    <h1><%= @post.title %></h1>
    <p class="meta">
      By <%= link_to @post.author.full_name, @post.author %>
      on <%= @post.published_at.strftime('%B %d, %Y') %>
    </p>
  </header>
  
  <div class="content">
    <%= sanitize @post.content %>
  </div>
  
  <% if @post.user == current_user %>
    <footer class="actions">
      <%= link_to 'Edit', edit_post_path(@post), class: 'button' %>
      <%= button_to 'Delete', @post, method: :delete, 
                    data: { confirm: 'Are you sure?' },
                    class: 'button danger' %>
    </footer>
  <% end %>
</article>

<section class="comments">
  <h2><%= pluralize(@comments.count, 'Comment') %></h2>
  <%= render @comments %>
  
  <% if user_signed_in? %>
    <%= render 'comments/form', post: @post %>
  <% end %>
</section>

<aside class="related">
  <h3>Related Posts</h3>
  <%= render partial: 'posts/post_summary', collection: @related_posts %>
</aside>

Partials enable View reuse by extracting common elements into separate files. Partial names begin with underscores, and Rails automatically locates and renders them.

<!-- app/views/posts/_post_summary.html.erb -->
<div class="post-summary">
  <h4><%= link_to post_summary.title, post_summary %></h4>
  <p><%= truncate(post_summary.content, length: 150) %></p>
  <span class="timestamp"><%= time_ago_in_words(post_summary.published_at) %> ago</span>
</div>

Helpers provide View-specific utility methods without cluttering Views with complex logic. Helper modules automatically include in Views and can be organized per Controller or shared globally.

# app/helpers/posts_helper.rb
module PostsHelper
  def formatted_publish_date(post)
    if post.published_at.today?
      "Today at #{post.published_at.strftime('%I:%M %p')}"
    elsif post.published_at > 1.week.ago
      "#{time_ago_in_words(post.published_at)} ago"
    else
      post.published_at.strftime('%B %d, %Y')
    end
  end
  
  def post_status_badge(post)
    status = post.published? ? 'published' : 'draft'
    content_tag(:span, status.capitalize, class: "badge badge-#{status}")
  end
  
  def categorized_posts_path(post)
    posts_path(category: post.category_id)
  end
end

API Controllers demonstrate MVC flexibility by providing different View representations. A single Controller action can respond with HTML or JSON based on request format.

class Api::V1::PostsController < Api::V1::BaseController
  def index
    @posts = Post.published.includes(:author)
    
    render json: @posts, each_serializer: PostSerializer
  end
  
  def show
    @post = Post.find(params[:id])
    
    render json: @post, serializer: PostDetailSerializer,
           include: ['author', 'comments']
  end
  
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      render json: @post, status: :created, location: api_v1_post_url(@post)
    else
      render json: { errors: @post.errors }, status: :unprocessable_entity
    end
  end
end

Design Considerations

Complexity Threshold determines when MVC adds value versus overhead. Simple applications with minimal logic benefit less from strict MVC separation. The pattern's organizational benefits emerge in applications with complex business rules, multiple user interfaces, or frequent changes to specific components. A single-page admin tool might not justify full MVC structure, while a multi-tenant SaaS application with web, mobile, and API clients gains significant advantages.

Model Responsibility Scope requires decisions about what belongs in Models versus external objects. Fat Models place all business logic within Model classes, leading to large files with many concerns. Skinny Models push logic into service objects, interactors, or command objects, keeping Models focused on data persistence. The choice depends on application complexity and team preferences. Rails defaults favor Fat Models for straightforward applications but encourage extraction as complexity grows.

View Logic Boundaries need clear definition. Views should format data and handle presentation, but complex formatting logic clutters templates. Presenter objects (also called decorators or view models) extract presentation logic from Views while keeping it separate from business logic. This three-layer approach—Model for business logic, Presenter for presentation logic, View for rendering—extends MVC for complex display requirements.

# Presenter pattern for complex View logic
class PostPresenter
  def initialize(post, view_context)
    @post = post
    @view = view_context
  end
  
  def formatted_content
    markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
    markdown.render(@post.content).html_safe
  end
  
  def reading_time
    words = @post.content.split.size
    minutes = (words / 200.0).ceil
    "#{minutes} min read"
  end
  
  def social_share_text
    "Check out: #{@post.title} by #{@post.author.username}"
  end
  
  def display_name
    @post.published? ? @post.title : "[Draft] #{@post.title}"
  end
  
  delegate :author, :published_at, :comments_count, to: :@post
end

# Controller
def show
  @post = PostPresenter.new(Post.find(params[:id]), view_context)
end

Controller Action Granularity affects maintainability. RESTful conventions suggest seven standard actions (index, show, new, create, edit, update, destroy), but complex workflows may require additional actions. Too many actions in one Controller signals potential extraction into separate Controllers. A single UsersController handling authentication, profiles, settings, and account management should split into SessionsController, ProfilesController, SettingsController, and AccountsController.

Data Retrieval Strategy impacts performance and code organization. Controllers that query multiple Models for a single View create N+1 query problems and complicate testing. Service objects or query objects consolidate data retrieval logic and optimize database access.

# Service object consolidates complex queries
class DashboardData
  def initialize(user)
    @user = user
  end
  
  def call
    {
      recent_posts: recent_posts,
      trending_topics: trending_topics,
      user_stats: user_stats,
      notifications: notifications
    }
  end
  
  private
  
  def recent_posts
    Post.published
        .includes(:author, :category)
        .order(published_at: :desc)
        .limit(10)
  end
  
  def trending_topics
    Topic.joins(:posts)
         .group('topics.id')
         .order('COUNT(posts.id) DESC')
         .limit(5)
  end
  
  def user_stats
    {
      posts_count: @user.posts.count,
      comments_count: @user.comments.count,
      followers: @user.followers.count
    }
  end
  
  def notifications
    @user.notifications.unread.order(created_at: :desc).limit(5)
  end
end

# Controller becomes simpler
class DashboardsController < ApplicationController
  def show
    @dashboard = DashboardData.new(current_user).call
  end
end

Alternative Architectures provide different trade-offs. The Ports and Adapters pattern (Hexagonal Architecture) inverts MVC dependencies, making business logic central with adapters for UI and data access. CQRS (Command Query Responsibility Segregation) separates read and write operations into distinct Models. Event-driven architectures decouple components through message passing. These alternatives suit specific requirements but add complexity beyond MVC's learning curve.

Form Objects handle complex input validation spanning multiple Models. Standard Rails forms map to single Models, but multi-Model forms require coordination logic. Form objects encapsulate this logic, validate input, and orchestrate Model updates.

class RegistrationForm
  include ActiveModel::Model
  
  attr_accessor :email, :password, :password_confirmation,
                :first_name, :last_name, :company_name
  
  validates :email, presence: true, email: true
  validates :password, length: { minimum: 8 }
  validates :password_confirmation, comparison: { equal_to: :password }
  validates :first_name, :last_name, :company_name, presence: true
  
  def save
    return false unless valid?
    
    ActiveRecord::Base.transaction do
      user = create_user
      create_organization(user)
      send_welcome_email(user)
      true
    end
  rescue ActiveRecord::RecordInvalid
    false
  end
  
  private
  
  def create_user
    User.create!(
      email: email,
      password: password,
      first_name: first_name,
      last_name: last_name
    )
  end
  
  def create_organization(user)
    Organization.create!(
      name: company_name,
      owner: user
    )
  end
  
  def send_welcome_email(user)
    UserMailer.welcome(user).deliver_later
  end
end

Common Patterns

Fat Models, Skinny Controllers concentrates business logic in Models while keeping Controllers minimal. Controllers handle HTTP concerns—parsing parameters, authentication, response formatting—and delegate domain operations to Models. This pattern prevents Controllers from becoming complex and difficult to test.

# Skinny Controller
class OrdersController < ApplicationController
  def create
    @order = current_user.orders.build(order_params)
    
    if @order.place_order
      redirect_to @order, notice: 'Order placed successfully'
    else
      render :new, status: :unprocessable_entity
    end
  end
end

# Fat Model
class Order < ApplicationRecord
  belongs_to :user
  has_many :line_items, dependent: :destroy
  
  validates :status, inclusion: { in: %w[pending confirmed shipped delivered] }
  
  def place_order
    return false unless valid_for_placement?
    
    transaction do
      self.status = 'confirmed'
      self.confirmed_at = Time.current
      calculate_total
      reserve_inventory
      charge_payment
      save!
      send_confirmation_email
    end
    
    true
  rescue PaymentError, InventoryError => e
    errors.add(:base, e.message)
    false
  end
  
  private
  
  def valid_for_placement?
    line_items.any? && shipping_address.present?
  end
  
  def calculate_total
    self.subtotal = line_items.sum(&:total)
    self.tax = subtotal * tax_rate
    self.total = subtotal + tax + shipping_cost
  end
  
  def reserve_inventory
    line_items.each do |item|
      item.product.reserve!(item.quantity)
    end
  end
  
  def charge_payment
    PaymentProcessor.charge(user.payment_method, total)
  end
  
  def send_confirmation_email
    OrderMailer.confirmation(self).deliver_later
  end
end

Service Objects extract complex operations into dedicated classes when Models become unwieldy. Service objects typically perform one operation, take dependencies through initialization, and return results explicitly. This pattern provides clear interfaces and testable boundaries.

class OrderPlacement
  def initialize(order, payment_processor: PaymentProcessor.new)
    @order = order
    @payment_processor = payment_processor
  end
  
  def call
    return failure('Invalid order') unless @order.valid?
    
    ActiveRecord::Base.transaction do
      reserve_inventory!
      process_payment!
      confirm_order!
      notify_customer!
    end
    
    success
  rescue StandardError => e
    failure(e.message)
  end
  
  private
  
  def reserve_inventory!
    @order.line_items.each do |item|
      item.product.reserve!(item.quantity)
    end
  end
  
  def process_payment!
    @payment_processor.charge(@order.user.payment_method, @order.total)
  end
  
  def confirm_order!
    @order.update!(status: 'confirmed', confirmed_at: Time.current)
  end
  
  def notify_customer!
    OrderMailer.confirmation(@order).deliver_later
  end
  
  def success
    Result.new(success: true, order: @order)
  end
  
  def failure(message)
    Result.new(success: false, error: message)
  end
  
  Result = Struct.new(:success, :order, :error, keyword_init: true) do
    def success?
      success
    end
  end
end

Query Objects encapsulate complex database queries, particularly those combining multiple conditions, joins, or aggregations. Query objects make queries reusable, testable, and reduce Controller and Model complexity.

class ProductSearch
  def initialize(relation = Product.all)
    @relation = relation.extending(Scopes)
  end
  
  def by_category(category_id)
    @relation = @relation.by_category(category_id) if category_id.present?
    self
  end
  
  def by_price_range(min, max)
    @relation = @relation.where(price: min..max) if min || max
    self
  end
  
  def in_stock
    @relation = @relation.where('quantity > ?', 0)
    self
  end
  
  def with_discount
    @relation = @relation.where('discount_percentage > ?', 0)
    self
  end
  
  def sort_by(column)
    @relation = @relation.order(column => :asc)
    self
  end
  
  def results
    @relation
  end
  
  module Scopes
    def by_category(category_id)
      where(category_id: category_id)
    end
  end
end

# Usage in Controller
def index
  @products = ProductSearch.new
                .by_category(params[:category])
                .by_price_range(params[:min_price], params[:max_price])
                .in_stock
                .sort_by(params[:sort] || 'name')
                .results
                .page(params[:page])
end

Concerns extract shared Model or Controller functionality into modules. Rails includes ActiveSupport::Concern to simplify module inclusion and dependency management. Concerns prevent duplication while maintaining single responsibility per file.

# app/models/concerns/taggable.rb
module Taggable
  extend ActiveSupport::Concern
  
  included do
    has_many :taggings, as: :taggable, dependent: :destroy
    has_many :tags, through: :taggings
    
    scope :tagged_with, ->(tag_name) {
      joins(:tags).where(tags: { name: tag_name })
    }
  end
  
  def tag_list
    tags.pluck(:name).join(', ')
  end
  
  def tag_list=(names)
    self.tags = names.split(',').map do |name|
      Tag.find_or_create_by(name: name.strip)
    end
  end
end

# Usage
class Post < ApplicationRecord
  include Taggable
  # Now has tagging functionality
end

class Article < ApplicationRecord
  include Taggable
  # Shares same tagging behavior
end

Decorator Pattern wraps Models to add presentation logic without modifying Model classes. Decorators implement the same interface as Models but enhance or modify specific methods for display purposes.

class UserDecorator < SimpleDelegator
  def display_name
    verified? ? "#{full_name}" : full_name
  end
  
  def member_since
    "Member since #{created_at.strftime('%B %Y')}"
  end
  
  def avatar_url(size: 80)
    gravatar_id = Digest::MD5.hexdigest(email.downcase)
    "https://gravatar.com/avatar/#{gravatar_id}?s=#{size}"
  end
  
  def formatted_bio
    return 'No bio provided' if bio.blank?
    
    markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
    markdown.render(bio).html_safe
  end
end

# Controller
def show
  user = User.find(params[:id])
  @user = UserDecorator.new(user)
end

Practical Examples

Blog Application demonstrates standard CRUD operations with MVC separation. The Post Model manages content and relationships, the PostsController handles user requests, and Views display posts in various formats.

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  belongs_to :category
  
  validates :title, presence: true, length: { maximum: 200 }
  validates :content, presence: true
  validates :slug, presence: true, uniqueness: true
  
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(published_at: :desc) }
  scope :in_category, ->(category_id) { where(category_id: category_id) }
  
  before_validation :generate_slug, on: :create
  
  def publish!
    update(published: true, published_at: Time.current)
  end
  
  def unpublish!
    update(published: false, published_at: nil)
  end
  
  private
  
  def generate_slug
    self.slug = title.parameterize if title.present?
  end
end

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index, :show]
  before_action :set_post, only: [:show, :edit, :update, :destroy, :publish]
  before_action :authorize_author, only: [:edit, :update, :destroy, :publish]
  
  def index
    @posts = Post.published.includes(:author, :category)
                  .recent
                  .page(params[:page])
  end
  
  def show
    @comment = Comment.new
    @related_posts = Post.published
                         .in_category(@post.category_id)
                         .where.not(id: @post.id)
                         .limit(3)
  end
  
  def new
    @post = current_user.posts.build
    @categories = Category.all
  end
  
  def create
    @post = current_user.posts.build(post_params)
    
    if @post.save
      redirect_to @post, notice: 'Post created'
    else
      @categories = Category.all
      render :new, status: :unprocessable_entity
    end
  end
  
  def edit
    @categories = Category.all
  end
  
  def update
    if @post.update(post_params)
      redirect_to @post, notice: 'Post updated'
    else
      @categories = Category.all
      render :edit, status: :unprocessable_entity
    end
  end
  
  def destroy
    @post.destroy
    redirect_to posts_path, notice: 'Post deleted'
  end
  
  def publish
    if @post.publish!
      redirect_to @post, notice: 'Post published'
    else
      redirect_to @post, alert: 'Could not publish post'
    end
  end
  
  private
  
  def set_post
    @post = Post.find(params[:id])
  end
  
  def authorize_author
    unless @post.author == current_user || current_user.admin?
      redirect_to root_path, alert: 'Not authorized'
    end
  end
  
  def post_params
    params.require(:post).permit(:title, :content, :category_id, :published)
  end
end

# app/views/posts/index.html.erb
<h1>Blog Posts</h1>

<div class="posts">
  <% @posts.each do |post| %>
    <article class="post-summary">
      <h2><%= link_to post.title, post %></h2>
      <p class="meta">
        By <%= link_to post.author.name, post.author %>
        in <%= link_to post.category.name, category_posts_path(post.category) %>
        on <%= post.published_at.strftime('%B %d, %Y') %>
      </p>
      <p><%= truncate(post.content, length: 200) %></p>
    </article>
  <% end %>
</div>

<%= paginate @posts %>

# app/views/posts/show.html.erb
<article class="post">
  <h1><%= @post.title %></h1>
  <p class="meta">
    By <%= link_to @post.author.name, @post.author %>
    on <%= @post.published_at.strftime('%B %d, %Y') %>
  </p>
  
  <div class="content">
    <%= simple_format @post.content %>
  </div>
  
  <% if @post.author == current_user %>
    <div class="actions">
      <%= link_to 'Edit', edit_post_path(@post) %>
      <%= button_to 'Delete', @post, method: :delete, 
                    data: { confirm: 'Are you sure?' } %>
      <% unless @post.published? %>
        <%= button_to 'Publish', publish_post_path(@post), method: :patch %>
      <% end %>
    </div>
  <% end %>
</article>

<section class="comments">
  <h2>Comments</h2>
  <%= render @post.comments %>
  
  <% if user_signed_in? %>
    <%= render 'comments/form', post: @post, comment: @comment %>
  <% end %>
</section>

E-commerce Cart shows MVC handling complex business operations with multiple Model interactions and transactional requirements.

# app/models/cart.rb
class Cart < ApplicationRecord
  belongs_to :user, optional: true
  has_many :cart_items, dependent: :destroy
  has_many :products, through: :cart_items
  
  def add_product(product, quantity = 1)
    current_item = cart_items.find_by(product: product)
    
    if current_item
      current_item.quantity += quantity
      current_item.save
    else
      cart_items.create(product: product, quantity: quantity)
    end
  end
  
  def remove_product(product)
    cart_items.find_by(product: product)&.destroy
  end
  
  def update_quantity(product, quantity)
    item = cart_items.find_by(product: product)
    return unless item
    
    if quantity <= 0
      item.destroy
    else
      item.update(quantity: quantity)
    end
  end
  
  def subtotal
    cart_items.sum { |item| item.product.price * item.quantity }
  end
  
  def tax_amount
    subtotal * 0.08
  end
  
  def total
    subtotal + tax_amount
  end
  
  def empty?
    cart_items.empty?
  end
  
  def clear
    cart_items.destroy_all
  end
  
  def checkout
    return false if empty?
    
    transaction do
      order = Order.create!(
        user: user,
        subtotal: subtotal,
        tax: tax_amount,
        total: total,
        status: 'pending'
      )
      
      cart_items.each do |item|
        order.order_items.create!(
          product: item.product,
          quantity: item.quantity,
          price: item.product.price
        )
      end
      
      clear
      order
    end
  end
end

# app/controllers/cart_items_controller.rb
class CartItemsController < ApplicationController
  before_action :set_cart
  before_action :set_product, only: [:create, :update]
  
  def create
    @cart.add_product(@product, params[:quantity].to_i)
    redirect_to cart_path, notice: 'Item added to cart'
  end
  
  def update
    @cart.update_quantity(@product, params[:quantity].to_i)
    redirect_to cart_path, notice: 'Cart updated'
  end
  
  def destroy
    product = Product.find(params[:id])
    @cart.remove_product(product)
    redirect_to cart_path, notice: 'Item removed'
  end
  
  private
  
  def set_cart
    @cart = current_cart
  end
  
  def set_product
    @product = Product.find(params[:product_id])
  end
end

# app/controllers/carts_controller.rb
class CartsController < ApplicationController
  def show
    @cart = current_cart
    @cart_items = @cart.cart_items.includes(:product)
  end
  
  def destroy
    current_cart.clear
    redirect_to root_path, notice: 'Cart cleared'
  end
end

# app/views/carts/show.html.erb
<h1>Shopping Cart</h1>

<% if @cart.empty? %>
  <p>Your cart is empty</p>
  <%= link_to 'Continue Shopping', products_path %>
<% else %>
  <table class="cart">
    <thead>
      <tr>
        <th>Product</th>
        <th>Price</th>
        <th>Quantity</th>
        <th>Subtotal</th>
        <th></th>
      </tr>
    </thead>
    <tbody>
      <% @cart_items.each do |item| %>
        <tr>
          <td><%= link_to item.product.name, item.product %></td>
          <td><%= number_to_currency item.product.price %></td>
          <td>
            <%= form_with url: cart_item_path(item.product), method: :patch do |f| %>
              <%= f.number_field :quantity, value: item.quantity, min: 1 %>
              <%= f.submit 'Update' %>
            <% end %>
          </td>
          <td><%= number_to_currency item.product.price * item.quantity %></td>
          <td>
            <%= button_to 'Remove', cart_item_path(item.product), 
                         method: :delete %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
  
  <div class="cart-summary">
    <p>Subtotal: <%= number_to_currency @cart.subtotal %></p>
    <p>Tax: <%= number_to_currency @cart.tax_amount %></p>
    <p class="total">Total: <%= number_to_currency @cart.total %></p>
  </div>
  
  <div class="cart-actions">
    <%= button_to 'Clear Cart', cart_path, method: :delete %>
    <%= link_to 'Proceed to Checkout', new_order_path, class: 'button primary' %>
  </div>
<% end %>

API Implementation shows MVC adapting to provide JSON responses while maintaining the same business logic and Model layer.

# app/controllers/api/v1/base_controller.rb
module Api
  module V1
    class BaseController < ApplicationController
      skip_before_action :verify_authenticity_token
      before_action :authenticate_api_user
      
      rescue_from ActiveRecord::RecordNotFound, with: :not_found
      rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
      
      private
      
      def authenticate_api_user
        token = request.headers['Authorization']&.split(' ')&.last
        @current_user = User.find_by(api_token: token)
        
        render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
      end
      
      def not_found(exception)
        render json: { error: exception.message }, status: :not_found
      end
      
      def unprocessable_entity(exception)
        render json: { errors: exception.record.errors }, status: :unprocessable_entity
      end
    end
  end
end

# app/controllers/api/v1/posts_controller.rb
module Api
  module V1
    class PostsController < BaseController
      def index
        @posts = Post.published
                     .includes(:author, :category)
                     .page(params[:page])
                     .per(20)
        
        render json: {
          posts: @posts.map { |post| serialize_post(post) },
          meta: pagination_meta(@posts)
        }
      end
      
      def show
        @post = Post.find(params[:id])
        render json: serialize_post(@post, include_content: true)
      end
      
      def create
        @post = @current_user.posts.build(post_params)
        
        if @post.save
          render json: serialize_post(@post), status: :created
        else
          render json: { errors: @post.errors }, status: :unprocessable_entity
        end
      end
      
      private
      
      def serialize_post(post, include_content: false)
        data = {
          id: post.id,
          title: post.title,
          slug: post.slug,
          published_at: post.published_at,
          author: {
            id: post.author.id,
            name: post.author.name
          },
          category: {
            id: post.category.id,
            name: post.category.name
          }
        }
        
        data[:content] = post.content if include_content
        data
      end
      
      def pagination_meta(collection)
        {
          current_page: collection.current_page,
          total_pages: collection.total_pages,
          total_count: collection.total_count
        }
      end
      
      def post_params
        params.require(:post).permit(:title, :content, :category_id)
      end
    end
  end
end

Common Pitfalls

Fat Controllers accumulate complex business logic, making them difficult to test and maintain. Controllers that validate data, calculate values, make multiple Model calls, and handle error cases become bloated and violate single responsibility.

# Problematic: Fat Controller
class OrdersController < ApplicationController
  def create
    # Too much logic in Controller
    @order = Order.new(order_params)
    @order.user = current_user
    @order.order_date = Time.current
    
    # Calculating in Controller
    subtotal = 0
    @order.order_items.each do |item|
      subtotal += item.price * item.quantity
    end
    @order.subtotal = subtotal
    @order.tax = subtotal * 0.08
    @order.total = subtotal + @order.tax
    
    # Multiple validations in Controller
    if @order.order_items.empty?
      flash[:error] = 'Cannot create empty order'
      render :new and return
    end
    
    if @order.total > current_user.credit_limit
      flash[:error] = 'Exceeds credit limit'
      render :new and return
    end
    
    # Payment processing in Controller
    begin
      PaymentGateway.charge(current_user.payment_method, @order.total)
    rescue PaymentError => e
      flash[:error] = "Payment failed: #{e.message}"
      render :new and return
    end
    
    if @order.save
      # Email logic in Controller
      OrderMailer.confirmation(@order).deliver_now
      InventoryService.reserve(@order)
      redirect_to @order
    else
      render :new
    end
  end
end

# Better: Move logic to Model/Service
class OrdersController < ApplicationController
  def create
    result = OrderCreation.new(current_user, order_params).call
    
    if result.success?
      redirect_to result.order, notice: 'Order created'
    else
      flash.now[:error] = result.error
      render :new, status: :unprocessable_entity
    end
  end
end

Logic in Views occurs when Views perform calculations, make decisions, or query databases directly. Views should receive prepared data from Controllers and focus solely on presentation.

<!-- Problematic: Business logic in View -->
<div class="order-summary">
  <% subtotal = 0 %>
  <% @order.items.each do |item| %>
    <% subtotal += item.price * item.quantity %>
    <p><%= item.product.name %>: <%= number_to_currency(item.price * item.quantity) %></p>
  <% end %>
  
  <% tax = subtotal * 0.08 %>
  <% total = subtotal + tax %>
  
  <p>Subtotal: <%= number_to_currency(subtotal) %></p>
  <p>Tax: <%= number_to_currency(tax) %></p>
  <p>Total: <%= number_to_currency(total) %></p>
  
  <% if current_user.premium? && total > 100 %>
    <p class="discount">10% premium discount applied!</p>
  <% end %>
</div>

<!-- Better: Data prepared in Controller/Model -->
<div class="order-summary">
  <% @order.items.each do |item| %>
    <p><%= item.product.name %>: <%= number_to_currency(item.total) %></p>
  <% end %>
  
  <p>Subtotal: <%= number_to_currency(@order.subtotal) %></p>
  <p>Tax: <%= number_to_currency(@order.tax) %></p>
  <p>Total: <%= number_to_currency(@order.total) %></p>
  
  <% if @order.discount_applied? %>
    <p class="discount"><%= @order.discount_description %></p>
  <% end %>
</div>

Tight Coupling between components prevents modification and testing. Controllers that directly instantiate specific Model classes or Views that reference Controller methods create dependencies that complicate changes.

# Problematic: Tight coupling
class ReportsController < ApplicationController
  def sales
    # Directly coupled to specific query implementation
    @data = SalesReport.new.generate_for_period(
      Date.parse(params[:start_date]),
      Date.parse(params[:end_date])
    )
    
    # Renders specific View with no flexibility
    render 'reports/sales_report', layout: 'reports'
  end
end

# Better: Dependency injection and loose coupling
class ReportsController < ApplicationController
  def sales
    date_range = parse_date_range(params)
    report_generator = report_generator_for(params[:type])
    
    @report = report_generator.generate(date_range)
    
    respond_to do |format|
      format.html
      format.pdf { render pdf: 'sales_report' }
      format.json { render json: @report }
    end
  end
  
  private
  
  def report_generator_for(type)
    case type
    when 'detailed' then DetailedSalesReport.new
    when 'summary' then SummarySalesReport.new
    else StandardSalesReport.new
    end
  end
  
  def parse_date_range(params)
    DateRangeParser.new(params[:start_date], params[:end_date]).parse
  end
end

Instance Variable Proliferation occurs when Controllers create numerous instance variables for Views, making it unclear what data the View actually requires and creating hidden dependencies.

# Problematic: Too many instance variables
class DashboardsController < ApplicationController
  def show
    @user = current_user
    @posts = @user.posts.recent.limit(5)
    @comments = @user.comments.recent.limit(10)
    @followers_count = @user.followers.count
    @following_count = @user.following.count
    @notifications = @user.notifications.unread
    @trending_topics = Topic.trending.limit(5)
    @featured_posts = Post.featured.limit(3)
    @user_stats = UserStatsCalculator.new(@user).calculate
    # View has access to all these variables but which does it use?
  end
end

# Better: Encapsulate in presentation object
class DashboardsController < ApplicationController
  def show
    @dashboard = DashboardPresenter.new(current_user)
    # Single object with clear interface
  end
end

class DashboardPresenter
  def initialize(user)
    @user = user
  end
  
  def recent_posts
    @recent_posts ||= @user.posts.recent.limit(5)
  end
  
  def recent_comments
    @recent_comments ||= @user.comments.recent.limit(10)
  end
  
  # Explicit methods show what data is available
end

Missing Validation at the Model level allows invalid data to persist. Relying on Controller or View validation creates opportunities for data corruption when models are accessed through different interfaces.

# Problematic: Validation only in Controller
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    
    # Controller validation is insufficient
    if params[:user][:email].blank?
      flash[:error] = 'Email required'
      render :new and return
    end
    
    if User.exists?(email: params[:user][:email])
      flash[:error] = 'Email taken'
      render :new and return
    end
    
    @user.save  # No Model validations!
    redirect_to @user
  end
end

# Better: Model validations are canonical
class User < ApplicationRecord
  validates :email, presence: true,
                    uniqueness: true,
                    format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 8 }, on: :create
  validates :username, presence: true,
                       uniqueness: { case_sensitive: false },
                       format: { with: /\A[a-zA-Z0-9_]+\z/ }
end

class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    
    if @user.save
      redirect_to @user, notice: 'Account created'
    else
      render :new, status: :unprocessable_entity
    end
  end
end

Reference

MVC Component Responsibilities

Component Primary Responsibilities Should Not Contain
Model Data persistence, validation, business logic, relationships Presentation logic, HTTP concerns, UI decisions
View Data presentation, HTML generation, formatting Business logic, data persistence, complex calculations
Controller Request handling, parameter parsing, response coordination Business rules, presentation logic, data validation

Rails MVC Conventions

Convention Location Purpose
Models app/models ActiveRecord classes, business logic
Controllers app/controllers Request handling, response coordination
Views app/views/controller_name Templates matching controller actions
Helpers app/helpers View utility methods
Concerns app/models/concerns, app/controllers/concerns Shared functionality modules
Serializers app/serializers JSON/XML representation logic

Common Model Methods

Method Type Examples Purpose
Validations validates, validates_presence_of, validates_uniqueness_of Data integrity enforcement
Callbacks before_save, after_create, around_update Lifecycle hooks
Scopes scope :active, default_scope Reusable query methods
Associations belongs_to, has_many, has_one Relationship definitions
Business Logic calculate_total, can_edit?, publish! Domain operations

Controller Action Patterns

Action HTTP Verb Path Purpose
index GET /resources List all resources
show GET /resources/:id Display single resource
new GET /resources/new Show creation form
create POST /resources Create new resource
edit GET /resources/:id/edit Show edit form
update PATCH/PUT /resources/:id Update existing resource
destroy DELETE /resources/:id Delete resource

View Helper Categories

Category Examples Use Case
Formatting number_to_currency, time_ago_in_words Display formatting
Links link_to, button_to, mail_to Navigation generation
Forms form_with, text_field, select_tag Form building
Assets image_tag, javascript_include_tag Asset management
Sanitization sanitize, strip_tags Security

Common Architectural Patterns

Pattern Purpose Implementation
Service Object Extract complex operations Single-purpose class with call method
Query Object Encapsulate complex queries Chainable interface returning relation
Form Object Handle multi-model forms ActiveModel::Model implementation
Presenter/Decorator View-specific logic Wraps model with presentation methods
Policy Object Authorization logic Encapsulates permission checks
Observer Decouple side effects Responds to model lifecycle events

Strong Parameters Pattern

# Required format for mass assignment protection
def resource_params
  params.require(:resource).permit(:attr1, :attr2, nested: [:id, :attr3])
end

RESTful Route Mapping

# config/routes.rb
resources :posts do
  member do
    patch :publish      # /posts/:id/publish
  end
  collection do
    get :featured       # /posts/featured
  end
  resources :comments  # Nested resource
end

Callback Execution Order

before_validation
after_validation
before_save
before_create (or before_update)
[database operation]
after_create (or after_update)
after_save
after_commit (or after_rollback)

View Rendering Options

Method Usage Purpose
render render :show Render specific template
redirect_to redirect_to @post HTTP redirect
render json: render json: @posts JSON response
render partial: render partial: 'form' Render partial template
render layout: render layout: 'admin' Custom layout

Testing Strategy

Component Test Type Focus
Model Unit tests Validations, associations, business logic
Controller Request tests HTTP responses, parameter handling
View View tests Rendered output, helper usage
Integration System tests End-to-end workflows