Skip to content

Presentation-Layer Architectural Patterns

These patterns answer the same question — how is the UI wired to the rest of the app? — but make different trade-offs about who owns state, who handles input, and how the View communicates with business logic.

The seven below are arranged roughly by historical lineage: MVC came first; MVP tried to make MVC's View testable; MVVM replaced manual updates with data binding; MVI/BLoC/Flux pushed toward unidirectional streams; VIPER took it furthest into strict role separation.

MVC — Model-View-Controller

Originated in Smalltalk-80 (1979) by Trygve Reenskaug. The View renders the Model. The Controller receives user input, mutates the Model, and tells the View to update.

flowchart LR
    User -- input --> Controller
    Controller -- updates --> Model
    Model -- notifies --> View
    View -- displays --> User
    Controller -. selects .-> View

Server-side MVC vs original MVC

The MVC you see in Rails/Laravel/Spring is a server-side adaptation: the Controller returns a rendered View on each request, so there's no live "Model notifies View" loop. The original Smalltalk MVC was a desktop UI pattern with the Observer relationship intact.

When to use

  • Server-rendered web apps where each request produces a fresh View.
  • Apps where input handling and rendering are genuinely separate concerns.
  • A reasonable default when you don't have a strong reason to pick something else.

Trade-offs

  • Pro: Universally understood; nearly every web framework ships with it.
  • Pro: Cleanly separates HTTP routing/input from rendering.
  • Con: "Controller" tends to grow — business logic leaks into it ("fat controller" anti-pattern).
  • Con: The original MVC's View↔Model coupling is hard to test on the desktop side.

MVP — Model-View-Presenter

A 1990s evolution of MVC for desktop UIs, popularized by Martin Fowler. The View becomes passive — it only renders what the Presenter tells it to. The Presenter holds presentation logic and is fully testable in isolation.

flowchart LR
    User -- input --> View
    View -- forwards events --> Presenter
    Presenter -- reads/writes --> Model
    Presenter -- updates --> View
    View -- displays --> User

When to use

  • Frameworks where the View is hard to unit-test (raw Android Activities, WinForms, GWT).
  • Codebases where you want presentation logic 100 % covered by tests without UI tooling.

Trade-offs

  • Pro: Presenter is plain old code — trivial to unit test.
  • Pro: View is a "humble object" with no logic worth testing.
  • Con: Lots of Presenter↔View interface boilerplate.
  • Con: Largely superseded by MVVM in frameworks with data binding.

MVVM — Model-View-ViewModel

Introduced by Microsoft in 2005 for WPF (John Gossman). The ViewModel exposes observable state; the View binds to it declaratively. When state changes, the framework updates the View automatically — no manual view.setText(...) calls.

flowchart LR
    User -- input --> View
    View -- two-way binding --> ViewModel
    ViewModel -- reads/writes --> Model
    Model -- emits changes --> ViewModel

Built on Observer

MVVM is essentially the Observer pattern baked into the UI framework. The ViewModel is the Subject; the View is the Observer; the data-binding engine wires them together at compile or runtime.

When to use

  • UI frameworks with first-class data binding: WPF, UWP, Jetpack Compose, Vue, Knockout, SwiftUI.
  • Modern Android with LiveData / StateFlow and Jetpack ViewModel.
  • Anywhere you want to eliminate the boilerplate of MVP's View↔Presenter calls.

Trade-offs

  • Pro: Much less glue code than MVP — binding does the wiring.
  • Pro: ViewModel has no UI dependencies, so it's still unit-testable.
  • Con: Two-way binding can hide bugs and make data flow hard to trace.
  • Con: Easy to leak business logic into ViewModels.

MVI — Model-View-Intent

Inspired by Cycle.js (André Staltz, ~2015) and adopted heavily in Android with Kotlin Flow. The View emits Intents (user actions); a reducer-like function turns them into a new immutable Model (state); the View renders that state. Strictly unidirectional.

flowchart LR
    View -- Intent --> Reducer
    Reducer -- new State --> Model
    Model -- emits --> View
    View -- renders --> User
    User -- input --> View

When to use

  • Reactive Android apps (Kotlin Flow, RxJava).
  • UIs where you need a reproducible, snapshot-able state at every moment (debugging, time-travel).
  • Teams already comfortable with stream-based programming.

Trade-offs

  • Pro: State is immutable and one-way — easy to reason about and replay.
  • Pro: Every screen state is a value you can log, persist, or test against.
  • Con: More ceremony than MVVM (action types, reducers, state classes).
  • Con: Reactive learning curve.

BLoC — Business Logic Component

The canonical Flutter pattern (introduced by Google at DartConf 2018). A BLoC takes Streams in, emits Streams out. The UI dispatches events; the BLoC processes them and emits new states; widgets rebuild when state changes.

flowchart LR
    UI[Widget] -- Event Stream (sink) --> BLoC
    BLoC -- reads/writes --> Repository
    Repository -- data --> BLoC
    BLoC -- State Stream (source) --> UI

BLoC is MVI in Flutter clothes

BLoC and MVI share the same DNA: events in, immutable state out, strictly unidirectional. BLoC is just the Flutter community's name and idiom for it, built on Dart Streams.

When to use

  • Flutter apps of any non-trivial size — it's effectively the framework's default architecture.
  • Any Dart code where Streams already feel natural.

Trade-offs

  • Pro: Strong separation of UI from logic; deeply testable.
  • Pro: First-class library support (flutter_bloc, bloc_test).
  • Con: Boilerplate for every screen (events, states, BLoC class).
  • Con: Can feel heavy for very simple screens.

Flux / Redux

Flux announced by Facebook in 2014; Redux simplified it in 2015 (Dan Abramov, Andrew Clark). One single store holds all app state. The View dispatches Actions; pure Reducers produce a new state; the View re-renders. Strictly unidirectional, single source of truth.

flowchart LR
    View -- Action --> Dispatcher
    Dispatcher -- Action --> Reducer
    Reducer -- new State --> Store
    Store -- subscribes --> View
    User -- input --> View

When to use

  • React apps with state shared across many components.
  • Apps where you need time-travel debugging or replayable actions.
  • Teams that want a strict, predictable mutation model.

Trade-offs

  • Pro: Single source of truth makes debugging easier (Redux DevTools).
  • Pro: Pure reducers are trivial to test.
  • Con: Lots of boilerplate (actions, action creators, reducers, selectors). RTK and Zustand exist precisely to address this.
  • Con: Overkill for small apps — local component state often suffices.

VIPER — View, Interactor, Presenter, Entity, Router

A strict iOS-flavored adaptation of Clean Architecture. Each module has five well-defined roles, each in its own file/class. Born from Mutual Mobile's blog post (2014) as a reaction to Massive View Controllers.

flowchart LR
    View -- input --> Presenter
    Presenter -- requests --> Interactor
    Interactor -- reads/writes --> Entity
    Interactor -- result --> Presenter
    Presenter -- viewmodel --> View
    Presenter -- navigate --> Router
    Router -- presents --> View
Role Responsibility
View Display, forward events
Interactor Business logic / use cases
Presenter Format data, decide what View shows
Entity Plain data models
Router Navigation between modules

When to use

  • Large iOS apps (10+ engineers) where strict module boundaries justify the boilerplate.
  • Codebases that need consistent structure across many screens.

Trade-offs

  • Pro: Crisp separation; everyone on the team writes the same shape of module.
  • Pro: Each role is independently testable.
  • Con: Heavy boilerplate — ~5 files per screen, even trivial ones.
  • Con: Often cargo-culted into apps that don't need it.

How to Choose Between These

flowchart TD
    Start[New UI screen] --> Q1{Server-rendered<br/>or SPA?}
    Q1 -- Server-rendered --> MVC
    Q1 -- SPA / Mobile --> Q2{Framework has<br/>data binding?}
    Q2 -- Yes --> Q3{State shared<br/>across screens?}
    Q2 -- No --> MVP
    Q3 -- Yes, lots --> Redux[Flux / Redux]
    Q3 -- Mostly local --> Q4{Want immutable<br/>unidirectional state?}
    Q4 -- Yes --> Q5{Flutter?}
    Q4 -- No --> MVVM
    Q5 -- Yes --> BLoC
    Q5 -- No --> MVI

VIPER is omitted from the flow on purpose: it's a structural style choice independent of these data-flow questions, usually adopted at the team level, not per-screen.