This website uses cookies

Read our Privacy policy and Terms of use for more information.

Pull up the docs for any web framework written in the last twenty years and you will find the same three letters. Rails calls itself MVC. Django says it is "MTV" but admits in the FAQ that this is MVC with the names shuffled. Spring MVC puts it in the package name. ASP.NET, Laravel, Phoenix, Angular in its early days, even SwiftUI's critics reach for the acronym to describe what it replaced. The pattern is so universal that it has stopped meaning anything specific. "We use MVC" tells you roughly as much as "we use files."

Here is the thing nobody mentions while reaching for the diagram: almost none of these frameworks implement the pattern that the name comes from. The original MVC, the one Trygve Reenskaug described at Xerox PARC in 1979 and that got built into Smalltalk-80, looks almost nothing like what Rails does. The Model knew nothing about the View, the View observed the Model directly, and the Controller handled mouse and keyboard input for a single widget. There was no HTTP request. There was no router. The Controller in a Rails app and the Controller in Smalltalk-80 are not the same idea wearing the same name; they are different ideas that collided into the same name.

The thesis is this: MVC did not survive as an architecture. It survived as a boundary. The one durable instruction inside the pattern is "keep the code that renders separate from the code that decides," and every framework that claims MVC is really just relitigating where to draw that line. The reason they all reinvent it is that the original drew the line in a place that does not exist on the web, so each generation had to draw it again. And the part they keep getting wrong, the part that keeps mutating into new acronyms, is always the Controller.

What the original actually was

Reenskaug's goal was narrow and worth stating precisely. He was building interactive tools at PARC and wanted to "bridge the gap between the human user's mental model and the digital model that exists in the computer." The Model was the domain object, your problem in code, with no idea that a screen existed. The View was a presentation of that model on the screen. The Controller was the thing that took raw input, a mouse click at pixel (412, 88), a keypress, and translated it into a meaningful operation.

The wiring is the part people forget. In Smalltalk-80 MVC, the View observed the Model directly through a dependency mechanism. When the Model changed, it broadcast a changed notification, and every View registered as a dependent received an update and redrew itself. The Controller did not sit in the middle of this loop. It was off to the side, catching input events and poking the Model. Data flowed Model to View without the Controller's involvement at all.

   input events
       |
       v
  +----------+        modifies        +----------+
  |Controller| --------------------> |  Model   |
  +----------+                        +----------+
       |                                  |
       | (creates/configures)             | changed: notification
       v                                  v
  +----------+      observes / reads  +----------+
  |   View   | <-------------------- |  (View is a dependent) |
  +----------+                        +------------------------+

Three objects, yes, but the relationships are specific. One Model could have many View-Controller pairs open on it at once, and when you changed the Model in one window, every other window updated itself because they were all observers. This is the actual machinery: the Observer pattern doing the heavy lifting, with Controller as a thin input adapter. If that sounds more like a desktop GUI than a website, that is because it was. MVC was a fat-client, stateful, in-memory pattern for a single user staring at a screen.

Now take that to the web and watch it fall apart.

Why the web broke it

HTTP is stateless and request-shaped. There is no long-lived View object sitting in memory observing a Model and redrawing when it changes. The server gets a request, builds a response, and forgets everything. The browser throws away the page on the next navigation. The entire observer loop, the thing that made Smalltalk MVC what it was, has nowhere to live.

So when the first web frameworks reached for MVC, they kept the three names and quietly rebuilt the meaning. In Rails, the Controller is not an input adapter for a widget. It is the entry point for a whole HTTP request. The router hands it a request, it talks to the Model, picks a View template, and renders a response. The View does not observe the Model; it is a template that gets handed data once and rendered to a string. The Model does not broadcast changes to anyone, because there is no one listening between requests.

# Rails: the "Controller" is an HTTP request handler, not an input adapter
class InvoicesController < ApplicationController
  def show
    @invoice = Invoice.find(params[:id])   # talk to the Model
    # the View (show.html.erb) is rendered with @invoice, once
  end
  def pay
    @invoice = Invoice.find(params[:id])
    @invoice.charge!(amount: params[:amount])   # mutate the Model
    redirect_to invoice_path(@invoice)          # no observer, just a new request
  end
end

This is a perfectly reasonable design. It is also not Reenskaug's MVC in any meaningful sense beyond the boundary: rendering (the ERB template) is separate from the request handling (the controller) is separate from the domain (Invoice). Django saw this clearly enough to rename the pieces. What Rails calls a Controller, Django calls a View; what Rails calls a View, Django calls a Template. Django's own documentation shrugs and says the framework is the Controller, so the developer-facing pieces are just Model, View, Template. Same boundary, different label, and an honest admission that the "C" had become something else.

The Controller was always the weak point

Here is the pattern inside the pattern. Model and View are conceptually stable. A Model is your domain. A View is a presentation. People argue about how thick each should be, but nobody is confused about what they are. The Controller is where every reinvention happens, because "the thing in the middle that coordinates" is a description of a hole, not an object. Anything can fill a hole.

Watch the acronyms breed. The reason Microsoft promoted MVP (Model-View-Presenter) for Windows Forms and early web forms is that the Controller had swollen into a "Presenter" that drove a passive View through an interface, so the View could be a dumb shell and the logic could be unit-tested without a UI. The reason MVVM (Model-View-ViewModel) took over WPF, then Knockout, then Vue and the entire reactive-frontend world, is that they brought the observer loop back. A ViewModel is, almost exactly, the Smalltalk Model-plus-Controller fused into one observable object, with data binding playing the role of the old changed/update mechanism. MVVM is closer to original MVC than web MVC is. The frontend reinvented the desktop pattern because the browser finally got stateful again.

// MVVM-ish: the ViewModel is observable, the View binds to it.
// This is the Smalltalk observer loop, reborn as "reactivity".
const invoice = reactive({
  amount: 0,
  status: "draft",
  pay() { this.status = "paid"; }   // mutate; the View re-renders itself
});
// <button @click="invoice.pay()">Pay</button>
// <span>{{ invoice.status }}</span>   <-- observes, updates automatically

Every one of these is a different answer to the same question: where does coordination live, and how does the View find out something changed? Reenskaug answered "Controller coordinates input, Observer handles change." The web's request model could not run an observer loop, so it deleted the Controller's original job and reassigned the name to "request handler." The reactive frontend got an observer loop back via a runtime, so it deleted the Controller again and called the survivor a ViewModel. Forty years of architecture and the Model and View kept their jobs the whole time. Only the middle keeps getting fired and rehired under a new title.

The smell this predicts

If the boundary is the real pattern and the Controller is the soft spot, then you can predict exactly how MVC codebases rot. They rot at the Controller, and they rot in one of two directions.

The first is the fat controller. Business logic that belongs in the Model leaks into the request handler because the controller is right there and it is easy. You end up with a 300-line create action doing validation, tax calculation, email dispatch, and audit logging, none of which has anything to do with HTTP. The tell is that you cannot test your domain logic without simulating a request. Rails teams learned this the hard way and the corrective slogan became "fat models, skinny controllers," then later "neither, extract a service object," because the real lesson is that the Controller should not own logic at all. It is an adapter. The moment it makes a business decision, the boundary has leaked.

The second is the logic-in-the-template leak going the other way. A view template that contains if user.subscription.tier == 'pro' && user.trial_ends_at > Time.now is doing domain reasoning in the presentation layer. Now the rule about who counts as a paying user lives in an ERB file, and the next person who needs that rule copies it into a second template. The boundary leaked toward the View.

Both smells are the same disease: the line between "decide" and "render" got crossed. That line is the entire content of the pattern. Everything else, the three boxes, the names, the diagrams, is scaffolding around that one rule.

Yes, but the three boxes still help

The honest objection: if the original MVC does not match the web and the Controller is a permanent question mark, why does almost every successful framework still ship the three-letter shape? Because the shape is a good default even when the original mechanics do not apply. Telling a new developer "requests go in a controller, domain logic goes in a model, rendering goes in a view" gives them three named places to put code and a rule for what goes where. That is genuinely valuable. Most code does not need a custom architecture; it needs a boring, conventional one that the next maintainer recognizes on sight. MVC, even the bastardized web version, delivers that.

The mistake is treating the acronym as a theory instead of a convention. When someone argues that their framework is "true MVC" or that yours is "not real MVC," they are arguing about a 1979 GUI pattern that their stateless HTTP app does not implement anyway. The argument is empty. The useful question is never "is this MVC," it is "is rendering separated from deciding, and is the Controller staying thin." If the answer to both is yes, the labels do not matter.

What to do with this on Monday

Stop defending the acronym and start defending the boundary. In your next code review, do not ask "is this the MVC way." Ask two concrete questions instead. First: can I test the domain decision in this feature without constructing a fake HTTP request? If not, logic has leaked into the controller, and it belongs in a model or a service object. Second: does any template contain a business rule that another part of the system also needs to know? If so, that rule has leaked into the view, and it belongs behind a method on the model.

And when the next framework arrives claiming to have finally fixed MVC, or to have transcended it, read its docs with one question in mind: where did they put the Controller's old job, and what did they rename the survivor? You will almost always find that they kept the Model, kept the View, and reinvented the thing in the middle. That reinvention is not a flaw in the frameworks. It is the pattern doing the only thing it ever reliably did, which is point at a boundary and dare you to hold the line.

Sources

Keep Reading