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 |