Software ArchitectureSystem Design

MVC Architecture: The Pattern Behind Every Major Web Framework — Deep Dive

TT
TopicTrick Team
MVC Architecture: The Pattern Behind Every Major Web Framework — Deep Dive

MVC Architecture: The Pattern Behind Every Major Web Framework — Deep Dive


Table of Contents


Origins: Trygve Reenskaug's Vision at Xerox PARC

Trygve Reenskaug developed MVC at Xerox PARC in 1978-79, while working on Smalltalk-80. His insight was that user interfaces have three fundamentally different concerns that should be separated:

  1. The domain object state (what the data is) — the Model
  2. How the state is presented to the user (what they see) — the View
  3. How user interactions change the state (what happens on click) — the Controller

His original observation: "The essential purpose of MVC is to bridge the gap between the human user's mental model and the digital model that exists in the computer." A user thinks of a bank account as a thing with a balance — the Model represents that concept. The ATM screen is the View. The button press is handled by the Controller.

When Ruby on Rails adopted MVC in 2004 and showed it could be implemented with convention-over-configuration, every web framework followed.


The Three Components: Model, View, Controller

The Model: Data + Business Rules

The Model is NOT just a database wrapper. It is the authoritative representation of your domain — including all business rules, validations, and state transitions:

ruby
# Rails Model — not just a database row:
class Order < ApplicationRecord
    belongs_to :customer
    has_many :order_items
    has_one :shipment
    
    # Business rules enforced by the Model:
    validates :customer, presence: true
    validates :status, inclusion: { in: %w[draft placed processing shipped delivered cancelled] }
    validate :cannot_cancel_shipped_order
    
    # State transition logic lives in the Model:
    def place!
        raise InvalidStateError unless status == 'draft'
        update!(status: 'placed', placed_at: Time.current)
        OrderMailer.confirmation(self).deliver_later
        inventory.reserve_items(order_items)
    end
    
    def cancel!(reason:)
        raise InvalidStateError if %w[shipped delivered].include?(status)
        update!(status: 'cancelled', cancelled_at: Time.current, cancel_reason: reason)
        inventory.release_items(order_items)
        payment.refund! if payment.charged?
    end
    
    # Computed properties:
    def total_amount
        order_items.sum(&:subtotal)
    end
    
    def overdue?
        status == 'placed' && placed_at < 24.hours.ago
    end
    
    private
    
    def cannot_cancel_shipped_order
        errors.add(:status, "cannot cancel an order that has shipped") if status_was == 'shipped'
    end
end

Model owns: domain logic, validations, state machine transitions, relationships, business computations.

Model does NOT own: HTTP concerns, session data, view formatting, pagination parameters.


The View: Presentation Only

The View formats data for the user — it never performs business logic:

erb
<%# Rails ERB View — only presentation logic %>
<% # ✅ Acceptable: formatting for display %>
<h1>Order #<%= @order.id %></h1>
<p>Status: <%= @order.status.humanize %></p>
<p>Total: <%= number_to_currency(@order.total_amount) %></p>

<% # ✅ Acceptable: conditional display based on state %>
<% if @order.overdue? %>
  <div class="alert alert-warning">This order needs attention</div>
<% end %>

<% # ❌ WRONG: Business logic in the View %>
<% if @order.status == 'placed' && @order.placed_at < 24.hours.ago %>
  <%# This calculation belongs in Order#overdue? method, not here %>
<% end %>

The Controller: Thin Coordinator

The Controller handles HTTP, calls the Model, and selects the View:

ruby
# Skinny Controller — 10 lines of coordination, no business logic:
class OrdersController < ApplicationController
    before_action :authenticate_user!
    before_action :set_order, only: [:show, :update, :cancel]
    
    def create
        @order = current_user.orders.build(order_params)
        if @order.save
            redirect_to @order, notice: 'Order created'
        else
            render :new, status: :unprocessable_entity
        end
    end
    
    def cancel
        @order.cancel!(reason: params[:reason])  # Business logic in Model
        redirect_to @order, notice: 'Order cancelled'
    rescue Order::InvalidStateError => e
        redirect_to @order, alert: e.message
    end
    
    private
    
    def set_order
        @order = current_user.orders.find(params[:id])
    end
    
    def order_params
        params.require(:order).permit(:delivery_address, order_items_attributes: [:product_id, :quantity])
    end
end

The Complete MVC Data Flow


Fat Model / Skinny Controller: The Most Important Rule

The most common MVC mistake: business logic leaks into controllers.

ruby
# ❌ FAT CONTROLLER (wrong):
def cancel
    order = Order.find(params[:id])
    
    # Business rules duplicated in every controller that cancels:
    if order.status == 'shipped'
        flash[:error] = "Can't cancel — already shipped"
        return redirect_to order
    end
    
    order.update!(status: 'cancelled')
    
    # Refund logic in controller:
    if order.payment_captured?
        Stripe::Refund.create(payment_intent: order.stripe_id)
    end
    
    InventoryService.release(order.items)
    OrderMailer.cancellation(order).deliver_now
    
    redirect_to order
end

# ✅ SKINNY CONTROLLER (correct):
def cancel
    @order.cancel!(reason: params[:reason])  # All logic in Model
    redirect_to @order, notice: 'Cancelled'
rescue Order::InvalidStateError => e
    redirect_to @order, alert: e.message
end

The skinny controller can test cancel! in a unit test with zero HTTP setup. The fat controller requires a full integration test with HTTP, session, DB, and Stripe mock just to test the "can't cancel shipped order" rule.


MVC vs MVVM vs MVP: When to Use Each

PatternController/Presenter/ViewModelData BindingBest For
MVCController — mediates between M and VManual (controller pushes to view)Server-side web (Rails, Django, Laravel)
MVPPresenter — one-to-one with ViewPresenter updates View explicitlyAndroid (traditional), WinForms
MVVMViewModel — exposes observable stateTwo-way automatic bindingiOS (SwiftUI), WPF, Angular
Flux/ReduxUnidirectional dispatcherOne-way (state → view)React SPAs, complex client state

React doesn't implement MVC — it implements a unidirectional data flow pattern where:

  • Component state = Model
  • JSX render = View
  • Event handlers = Controller
  • useState/useReducer = State management

The underlying principle (separate data from presentation from interactions) is identical to MVC; the implementation is adapted for reactive, component-based UIs.


Frequently Asked Questions

Can MVC scale to large applications? Standard MVC struggles past a certain complexity threshold — the Model layer becomes a "God Object" with too many responsibilities. The solution is layered architecture: add a Service Layer between the Controller and Model for complex business workflows that span multiple models. Controllers call Services; Services orchestrate Models. This is "MVC + Service Layer" and is how large Rails (GitHub, Shopify) and Django applications are structured in practice.

Is MVC relevant in the era of microservices? Absolutely. MVC is a module-level pattern; microservices is a system-level pattern. Each microservice typically uses MVC (or MVVM/Clean Architecture) internally for organising its own code. The patterns aren't competing — they operate at different levels of abstraction.


Key Takeaway

MVC endures after 45+ years because the problem it solves — separating data, presentation, and user interaction handling — is a fundamental concern of every GUI application ever built. The concrete form changes (Smalltalk → Rails → React), but the separation of concerns principle is constant. Master MVC's correct implementation — especially the Fat Model / Skinny Controller discipline — and you'll write cleaner, more testable code in any framework that follows it.

Read next: Layered (N-Tier) Architecture: The Enterprise Standard →


Part of the Software Architecture Hub — comprehensive guides from architectural foundations to advanced distributed systems patterns.