CodeMVCMVPMVVMcontrollerview

MVC, MVP and MVVM

Model–View–Controller and its descendants split "what the data and rules are" from "how it is shown" from "how input becomes a change" — the pattern every web framework ships with, and the one most often reduced to a folder layout while the rules quietly move into the controller.

Interview question
Progress
What problem does this solve?

A screen whose rendering, input handling and business rules are one tangle cannot be tested without a browser, cannot be reused by a second client, and changes its rules whenever someone restyles it. MVC names three responsibilities and fixes who may talk to whom, so the rules live in the model and the view is replaceable.

Three roles, and who is allowed to know whom

The model is the application state and the rules over it: the order, its lines, the rule that it cannot ship unpaid. It knows nothing about screens or requests. The view renders the model for a human — HTML, a mobile screen, a terminal table — and knows nothing about business rules. The controller turns input into intent: it parses a request or a click, asks the model to change, and picks which view to show. In the original Smalltalk form the view observed the model and redrew itself; in the web form that most people mean, the controller hands the model (or a slice of it) to a template and the response is the rendered result.

The rule that makes it a pattern rather than three folders is the same one as in Layered Architecture: dependencies point at the model. Views and controllers depend on the model; the model depends on neither. A model that imports the templating engine, or a controller that computes a discount, has broken the pattern even if the files sit in the right directories. MVC is a presentation-layer pattern — it says nothing about persistence, transactions or services, which is why it composes with layered, Clean Architecture and Hexagonal Architecture (Ports & Adapters) rather than competing with them.

The view and the controller both point at the model; the model points at nothing
inputupdateselectreadsUser input (click, request)Controller (interpret intent)View (render)Model (state + rules)
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system

MVP and MVVM: moving the view's logic out of the view

MVC leaves one question open: where does *presentation* logic go — the decision that a negative balance is shown in red, that a date is formatted for the locale, that the submit button is disabled while a request is in flight? It is not a business rule, so it does not belong in the model; it is logic, so it does not belong in a template. MVP answers with a presenter that owns all of it and drives a passive view through an interface: the view has setters like showTotal(text) and forwards events, and the presenter is unit-testable with a fake view. MVVM answers with a view model, an object whose properties *are* the screen state — totalText, isSubmitEnabled, errorMessage — and a data-binding layer that keeps the view in sync, so the presenter's view.showTotal(...) calls disappear into the binding.

The three are the same idea at different points on one axis: how much the view knows. MVC views read the model directly and hold their own small logic. MVP views know nothing and are told what to display. MVVM views bind to a state object and the framework does the telling. Which one a codebase uses is usually decided by the framework, not by the team: server-rendered stacks (Rails, Django, Spring MVC, ASP.NET MVC) are MVC; classic Android and WinForms code is MVP; SwiftUI, Jetpack Compose, Vue and Knockout are MVVM-shaped because binding is the primitive they give you.

MVVM: the view binds to a view model; only the view model talks to the model
eventscallsdata bindingView (binds, no logic)View model (screen state)Model (rules)
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system
Same three responsibilities, different owner for presentation logic
MVCMVPMVVM
Who holds presentation logicView (a little) and controllerPresenterView model
How the view updatesRe-render from the modelPresenter calls view methodsData binding to view-model properties
View knows the model?Yes, reads itNo — passiveNo — sees only the view model
Unit-test the screen logicAwkward: needs a rendered viewPresenter with a fake viewView model alone, no UI
Typical homeServer-rendered web frameworksClassic Android, desktop toolkitsSwiftUI, Compose, Vue, WPF

Fat controllers, and where MVC sits in the layered picture

The failure that defines this pattern in practice is the fat controller. The first version of placeOrder parses the request and calls the model. The second version adds a stock check, because that was the quickest place to put it. The third adds the discount rule, the email, and a retry. Two years later the controller is 900 lines, the nightly batch job that also places orders has none of those rules because it never goes through HTTP, and the "model" is a bag of database columns — the anemic model that Layered Architecture warns about, seen from the other side. Nothing in the framework stops this; the framework only gave you a folder called controllers.

The fix is to read MVC through the layers. The controller is the presentation layer: it belongs to one delivery mechanism and should contain only what is specific to it — parsing, status codes, choosing a view. Everything that must also be true for the batch job, the queue consumer and the admin tool is a rule, and rules go in the model or the application service the controller calls. A useful test: if you deleted the web framework tomorrow, how much logic would you lose? The right answer is "the controllers, and nothing else".

  • A controller should be deletable without losing a rule; if deleting it loses one, the rule was in the wrong place.
  • Presentation logic (formatting, enablement, which error to show) belongs in a presenter or view model, not in the model and not in the template.
  • One model, many views: the same Order renders as HTML, JSON and a PDF invoice; the same controller does not.
Three entry points, one set of rules: whatever lives only in the HTTP controller is skipped by the other two
rules run hereHTTP controllerNightly batch jobQueue consumerApplication service (place order)Model: stock check, discount rule
ClientGateway / LBServiceWorkerDatabaseCacheQueue / LogObject storageCDNExternal system
A thin controller: transport in, intent out, nothing else
1// controller — belongs to HTTP, knows status codes, knows no rules
2export async function placeOrder(req: Request, res: Response) {
3 const result = await orders.place(req.user.id, parseItems(req.body))
4 if (!result.ok) return res.status(422).render('order/form', { errors: result.errors })
5 return res.redirect(`/orders/${result.id}`)
6}
7
8// model — the rules, reusable by the batch job and the queue consumer
9export class Order {
10 static create(userId: string, items: Item[]): Result<Order> {
11 if (items.length === 0) return err('empty_order')
12 if (items.some((i) => i.qty <= 0)) return err('invalid_quantity')
13 return ok(new Order(userId, items))
14 }
15}
16
17// view model (MVVM flavour) — screen state, no rendering, no rules
18export class OrderFormVM {
19 constructor(private items: Item[] = []) {}
20 get totalText() { return formatMoney(this.items.reduce((s, i) => s + i.price * i.qty, 0)) }
21 get canSubmit() { return this.items.length > 0 }
22}

Key points

  • Model holds state and rules, view renders, controller turns input into intent; both view and controller depend on the model, the model depends on neither.
  • MVC, MVP and MVVM differ only in who owns presentation logic: a bit of view plus controller, a presenter driving a passive view, or a bound view model.
  • MVC is a presentation-layer pattern; it composes with layered, clean and hexagonal architecture instead of replacing them.
  • The fat controller is the defining failure: rules that live only where HTTP arrives are skipped by every other entry point.
  • Test of health: deleting the web framework should lose the controllers and nothing else.

How data moves through it

One request or event, hop by hop.

  1. 1Input → Controller: the request or event is parsed into typed intent; authentication and shape validation happen here.
  2. 2Controller → Model: a method on the model or an application service is called with plain values; rules run and either succeed or return a violation.
  3. 3Model → View: the view (or presenter / view model) reads the resulting state and turns it into something a human can see.
  4. 4View → Output: the rendered HTML, JSON or screen goes back over the same transport the controller received the input on.

When to use — and when not

Use it when
  • Any application with a user interface and more than one screen; the framework you use already assumes it, so learn the pattern rather than fighting it.
  • When the same model must be shown in several ways — HTML, JSON API, export — and the rules must not be duplicated per view.
  • MVP or MVVM specifically when screen logic is rich enough to need unit tests without a rendered UI.
Avoid it when
  • A single-page script or a report generator with no interaction; three roles for one screen is ceremony.
  • As a substitute for an application or domain layer: MVC says nothing about transactions, services or persistence, and pretending it does breeds fat controllers.
  • MVVM where the framework has no binding primitive; hand-written binding code is a presenter with extra steps.

Tradeoffs

Complexity
low → high
Ops cost
low → high
Latency
low → high
Consistency
weak → strong
Scalability
poor → strong

Free at runtime. The cost is discipline: the folders exist on day one, the rules drift into controllers by month six unless someone reads diffs for it.

How it fails

  • Fat controller: the stock check, the discount rule and the email all land in the HTTP handler; the batch importer places orders that break every one of them.
  • Anemic model: entities are column bags with getters, so "the model" has no rules to protect and the pattern is three folders of pass-through.
  • Logic in templates: a discount computed inside the view for one page, computed differently in the API serializer, and the two disagree in production.
  • Massive view controller (mobile MVC): the screen class owns networking, parsing, caching and layout at once, and cannot be tested without a device.
  • View model that talks to the database: MVVM without a model layer, so the "view model" is a fat controller with data binding.

How it scales

  • Runtime scaling is untouched: MVC is in-process structure, and the deployable unit is still one process copied behind a load balancer (Monolithic Architecture).
  • It scales the number of views cheaply: a new client (mobile app, partner API) is a new view and controller over the same model, provided the rules actually live in the model.
  • When controllers multiply and share use cases, the next step is an application layer between controllers and model (Layered Architecture) so the shared orchestration has one home.

How it interacts with databases, queues, caches, APIs and external systems

  • Database: never touched by a view or a view model; reached through the model or a repository the application service owns.
  • JSON API: a second view over the same model — a serializer is a view, and it must not recompute rules the HTML view already relies on.
  • Queue consumers and batch jobs: alternative controllers that call the same model methods, which is exactly why the rules cannot live in the HTTP controller.
  • Client-side frameworks: an MVVM front end binds to a view model fed by a JSON view of the server model; the two "M"s are different objects and should not share code.