Technology Sep 15, 2026 · 9 min read

MVC, MVP, MVVM, MVVM-C, VIPER: one pattern wearing five outfits

Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us to help devs discover the project, give it a try, and share your feedback to help improve the product. Every mobile team eventually has the same argument. Someo...

DE
DEV Community
by Athreya aka Maneshwar
MVC, MVP, MVVM, MVVM-C, VIPER: one pattern wearing five outfits

Hello, I'm Maneshwar, and I'm building LiveReview — a blast-radius aware AI code review built for your business-critical systems. Star us to help devs discover the project, give it a try, and share your feedback to help improve the product.

Every mobile team eventually has the same argument.

Someone says the ViewController is 3,000 lines long and needs to be broken up.

Someone else says "just use MVVM," like that phrase alone fixes anything.

A third person mentions VIPER and the room goes quiet, because everyone has a VIPER war story.

I went down this rabbit hole recently and realized something that should have been obvious years ago: MVC, MVP, MVVM, MVVM-C and VIPER are not five different ideas.

They're one idea, wearing five different amounts of clothing.

The one shape underneath all of them

Strip away the acronyms and every pattern is arguing about the same three things.

A View, which is the face of the app. It renders pixels and captures your taps.

A Model, the brain, which owns the business logic and the data.

And a translator in between, whose entire job is making sure the View and the Model never talk to each other directly.

That translator is the part that keeps getting renamed. Controller. Presenter. View-Model. Everything else is a debate about how much power to give it, and who else gets to share the job.

Diagram: View, a translator layer, and Model, the shape every one of these patterns reuses

Model-View-Controller is genuinely old, coming out of Smalltalk work at Xerox PARC in the late 1970s, which makes it nearly as old as the personal computer itself.

It was built to separate "what the data is" from "what's on screen," and for a single-window desktop app in 1978, that was already a big improvement over one giant blob of code that did everything.

To see where each pattern actually differs, let's run the exact same tiny feature through all five: a user tapping their profile picture to pick a new one.

MVC: everything reports to one controller

MVC connects the View and the Model through a Controller that does all the coordinating.

You tap the picture, the View tells the Controller, the Controller updates the Model, and once that's done the Controller turns around and tells the View to refresh.

Diagram: View notifies Controller, Controller updates Model, Controller refreshes View

It reads clean on a whiteboard, and honestly it's a fine choice for a small app. That's not a knock, it's the actual selling point.

class ProfileController {
  onPhotoPicked(image) {
    model.setAvatar(image)      // update the model
    view.render(model.avatar)   // tell the view to refresh
  }
}

The catch is that this Controller doesn't stay this small. Every new feature wants to talk to the Model somehow, and the Controller is the only door in the building.

Six months later it's not a controller, it's a lobby that every department in the company has to walk through.

MVP: the Presenter takes the homework off the View

MVP answers the bloated-controller problem by handing UI logic to a dedicated Presenter, and telling the View to do nothing but draw.

Tap the photo, the View notifies the Presenter, the Presenter updates the Model, formats whatever comes back into something display-ready, and pushes that formatted result straight into the View.

Diagram: View notifies Presenter, Presenter updates Model and formats data back to View

The View in MVP is deliberately dumb, which is a compliment here. A dumb View is a View you can unit test the Presenter against without spinning up any UI at all, since the Presenter only ever talks to a thin interface the View implements.

That testability is the entire reason teams reach for MVP. Not because it's fancier than MVC, but because "does the right thing happen" becomes a question you can answer without a simulator or a device.

MVVM: the View stops asking, and starts just knowing

MVVM swaps the Presenter's manual "update the view" call for two-way data binding between the View and a View-Model.

Pick a new photo, the View pushes that change into the View-Model through binding, the View-Model persists it to the Model, and when the Model's data changes, that change flows back to the bound View property automatically. No explicit refresh call, anywhere.

Diagram: View and ViewModel bound both ways, ViewModel and Model bound both ways

This is the pattern that plays nicest with reactive frameworks, because reactive frameworks are basically data binding with better marketing.

class ProfileViewModel {
  @Observable var avatar: Image   // View binds to this directly

  func onPhotoPicked(image: Image) {
    avatar = image        // View updates instantly via binding
    model.persist(image)  // no explicit "refresh the view" call anywhere
  }
}

Less boilerplate, definitely. The tradeoff shows up later, when a bug means tracing a chain of automatic notifications instead of reading a linear function call, which is a genuinely different debugging skill.

MVVM-C: somebody still has to decide where the app goes next

MVVM never says who's in charge of moving between screens, and by default that job quietly lands on the View-Model, which is exactly the thing MVVM was trying to keep simple.

MVVM-C fixes that by adding a Coordinator that sits above the binding triangle and owns navigation on its own. In our example, that's the move from the profile screen to the image picker and back, including whatever save logic happens in between.

Diagram: Coordinator controls the ViewModel, same binding triangle as MVVM underneath

Same View, View-Model, Model relationship as plain MVVM. The only new thing is that "where do we go next" has its own dedicated home, instead of leaking into whichever View-Model happened to trigger the transition.

VIPER: five jobs, five files, no exceptions

VIPER takes the "give everyone exactly one job" idea about as far as it can go.

View displays and forwards user actions. Interactor owns the business logic. Presenter prepares data for the View and talks to the Interactor. Entity is the raw data, the Model equivalent. Router owns navigation.

Tap the photo: View tells Presenter, Presenter tells Interactor to do the actual work, Interactor manages the Entity, Presenter formats the result back for the View, and if a screen change is needed, Router handles it.

Diagram: View, Presenter, Interactor, Entity and Router, five components each with one job

protocol PhotoInteractorProtocol {
  func updateAvatar(_ image: Image)
}
protocol PhotoPresenterProtocol {
  func didPickPhoto(_ image: Image)
  func didUpdateAvatar(_ image: Image)
}
protocol PhotoRouterProtocol {
  func closePhotoPicker()
}

Notice there's no shared base class holding this together, just protocols. That's the whole VIPER pitch: maximum separation, maximum testability, and yes, maximum file count for updating a single profile picture.

It earns its keep on genuinely large apps with multiple teams working the same codebase. On a five-screen app it's mostly ceremony.

So which one do you actually pick

None of them are wrong, they're just answers to different problems.

MVC for a small app where shipping fast matters more than architecture purity.

MVP once you need the mediator to be testable in isolation but don't need data binding.

MVVM once your framework is reactive and binding removes real boilerplate.

MVVM-C once that same app also has navigation flows worth centralizing.

VIPER once the app and the team are both big enough that "everyone has one job" stops being overhead and starts being the thing that keeps ten engineers from stepping on each other.

flowchart TD
    A[Starting a new client app] --> B{Small app, small team?}
    B -->|Yes| MVC[MVC: simplicity wins, ship it]
    B -->|No| C{Need the mediator unit-testable?}
    C -->|Yes, no data binding needed| MVP[MVP: Presenter tests clean in isolation]
    C -->|Need reactive data binding| D{Complex navigation flows too?}
    D -->|No| MVVM[MVVM: binding kills boilerplate]
    D -->|Yes| E{Large app, many teams?}
    E -->|Not yet| MVVMC[MVVM-C: Coordinator owns navigation]
    E -->|Yes| VIPER[VIPER: five single-job pieces, max modularity]

    classDef decision fill:#f4d35e,stroke:#b8991f,color:#1a1a1a
    classDef start    fill:#e9ecef,stroke:#6c757d,color:#1a1a1a
    classDef pick     fill:#5ee6c8,stroke:#1f9c86,color:#1a1a1a
    classDef heavy    fill:#9d8cff,stroke:#5b4bcc,color:#1a1a1a

    class A start
    class B,C,D,E decision
    class MVC,MVP,MVVM pick
    class MVVMC,VIPER heavy

The pattern isn't the architecture. It's just how much ceremony your team is willing to pay for, in exchange for how much chaos it's trying to avoid.

Pick the smallest one that still lets you sleep at night, and upgrade only when the pain is real, not hypothetical.


Your team's attention is limited, and the deluge of AI-generated code is making it harder to keep production secure and reliable without slowing you down.

I'm building LiveReview, a blast-radius aware AI code review built for your business-critical systems.

Instead of presenting every diff with equal emphasis, LiveReview scores each change by blast radius — how far its impact reaches through your call graph — so you can focus attention where it actually matters.

Spend code review effort where business risk is highest — not spread evenly across every diff.

⭐ Star it on GitHub:

GitHub logo HexmosTech / LiveReview

Blast-Radius Aware AI Code Review for Business-Critical Systems

LiveReview

gitleaks.yml osv-scanner.yml govulncheck.yml semgrep.yml dependabot-enabled mcp-testcases.yml

LiveReview: Blast-Radius Aware AI Code Review for Business-Critical Systems

LiveReview is an AI code reviewer that scores every hunk of a diff by blast radius: how far a change reaches through your call graph, how much persistent state it touches, and how well-tested it is. A 3-line change to a shared auth check can outrank a 300-line UI tweak. Your team's attention goes to the highest-risk code first, not spread evenly across every diff.

blast-radius-demo.mp4

LiveReview's Blast Radius & Review Priority scoring, live in the diff viewer.
















The exact math, not a black box Visualize blast radius at a glance Every factor that feeds the score

How does Blast Radius scoring work? (a more technical explanation)

Here's the goal:

  • A 3-line fix in a function used by 40 other files, that also writes to a database, should score high.
  • A 300-line UI change in one file, fully covered by…




Click below to try LiveReview with your codebase:

LiveReview Banner

DE
Source

This article was originally published by DEV Community and written by Athreya aka Maneshwar.

Read original article on DEV Community
Back to Discover

Reading List