Across MVC, MVP, and MVVM, the Controller/Presenter/ViewModel all sit between View and Model, but none of them is supposed to hold real business logic. In practice, why does business logic keep leaking into that middle layer, and how would you architecturally prevent it at scale?
answer
- fat Controller/Presenter/ViewModel = same failure, different names
- path-of-least-resistance under deadline
- no compiler-enforced boundary by default
- extract domain/use-case layer
- ArchUnit-style module rules as enforcement
basics
~20 sBusiness rules keep sneaking into the Controller/Presenter/ViewModel because that's the easiest place to add 'just one more check' while wiring a screen. The fix is a separate, UI-independent layer, like domain services, that owns the rules, with the middle layer just calling it.
solid answer
~50 sThe middle layer is architecturally meant to coordinate, not decide - it translates input into Model calls and Model output into displayable state, and nothing more. Logic leaks into it because it's the path of least resistance: the person adding a feature is already editing that file to wire a new field, so adding a validation check right there is one line versus creating a new domain service class. This compounds because there's rarely a compiler-enforced boundary stopping it. At scale, the fix is process plus structure: extract real business rules into a separate domain/service layer with its own tests, independent of any UI-pattern class, and treat conditional-heavy logic appearing in a Presenter/ViewModel/Controller as a code-review flag that it likely belongs elsewhere. Some teams enforce this with module boundaries or lint rules that fail the build if a Presenter/ViewModel file imports network/database types directly.
go deeper
Recognizes that putting too much logic in one file is bad practice, in general terms, without necessarily naming the pattern-specific vocabulary.
Names the fat Controller/Presenter/ViewModel anti-patterns as instances of the same underlying problem and can suggest extracting a separate class to hold the logic.
Can distinguish UI-state mapping logic, which belongs in the coordination layer, from genuine business logic, which doesn't, and proposes a concrete refactor such as use-case/service extraction with tests.
Designs organization-level enforcement, such as module boundaries, architecture-linting, and review norms, that prevents the leak proactively across many teams and codebases, and can justify the ceremony/friction trade-off explicitly rather than mandating it dogmatically.
## One layer under three names The Controller in MVC, the Presenter in MVP, and the ViewModel in MVVM are structurally analogous in one crucial respect: all three are meant to be a **thin coordination layer**, translating between the vocabulary of user interaction (clicks, form input, navigation events) and the vocabulary of the domain (place an order, validate a password, compute a discount). None of them is supposed to decide business outcomes — that's the Model's job — they're supposed to orchestrate the conversation between View and Model. In practice, though, all three suffer the identical failure mode under a different name: | Pattern | The name the failure goes by | |---|---| | MVC | fat Controller | | MVP | fat Presenter | | MVVM | bloated ViewModel | This isn't a coincidence of naming; it's the same underlying dynamic playing out in three different pattern vocabularies. ## The mechanism behind the leak The mechanism behind the leak is almost always **convenience under time pressure** combined with the absence of an enforced boundary. When an engineer is adding a new field to a form, they are, by construction, already editing the Presenter or ViewModel file to wire that field's display and input handling. If that field needs a validation rule, the path of least resistance is to write that check right there, inline, in the same file they're already touching — versus stopping, creating or extending a separate domain/validation service class, writing a test for it, and then calling it from the Presenter/ViewModel. The second path is architecturally correct but requires more upfront discipline, and most languages and frameworks provide zero compiler-level enforcement that a Presenter/ViewModel/Controller cannot contain business logic — it's just a convention, and conventions erode under deadline pressure without active guardrails. ## Why it matters Why this matters concretely: business logic embedded in the coordination layer becomes **coupled to the platform**. - A validation rule written inside an Android ViewModel can't be reused by an iOS ViewModel or a backend batch job that needs the same rule, forcing that logic to be reimplemented, and inevitably to drift out of sync, in every platform-specific coordination layer that needs it. - It also becomes harder to test in isolation from UI-adjacent concerns — a rule buried inside a 200-line ViewModel method that also handles loading states and navigation now needs its test to set up all of that surrounding machinery just to verify a simple business invariant. ## The trade-off in fighting it The trade-off in fighting this leak is friction versus architectural integrity. Enforcing a hard boundary — a separate domain/service module that the Controller/Presenter/ViewModel is only allowed to call, never to duplicate logic from — adds ceremony: more files, more indirection, more places to look when tracing a bug. For a small app or an early-stage product where requirements are still churning rapidly, that ceremony can genuinely slow a team down more than it helps, which is a legitimate reason some teams tolerate mild logic bleed early on. The cost shows up later: as an app grows, the coordination-layer classes that were allowed to accumulate business logic become the hardest classes in the codebase to safely modify, because touching them risks breaking rules that are invisible outside that specific file. ## The fix at scale At scale, the architectural fix has both a structural and a process component. - **Structurally**, teams extract a genuine domain/service layer — plain classes or functions with no dependency on any UI-pattern base class, no reference to View/Presenter/ViewModel types, testable with plain unit tests and, ideally, shared across platforms. - Some organizations go further and **enforce the boundary mechanically**: module visibility rules, architecture linting tools such as ArchUnit-style rules that fail a build if a class in a `viewmodel` package imports from a `persistence` package directly, or code-review checklists that flag any conditional block containing business terminology inside a Presenter/ViewModel file as a signal to extract it. - **Process-wise**, the review culture matters as much as tooling: reviewers who reflexively ask 'why does the Presenter know about this rule' when they see business logic in that layer are enforcing the boundary socially where the compiler can't. ## A concrete instance A concrete real-world instance of this discipline: Clean Architecture and Hexagonal Architecture-influenced Android/iOS codebases commonly introduce an explicit 'use case' or 'interactor' layer sitting between the ViewModel and the Model/repository layer specifically to hold business rules, with the ViewModel reduced to calling a use case and mapping its result to UI state — directly addressing the leak this question describes by giving business logic an obvious, dedicated home that isn't the coordination layer.
- Why doesn't simply telling engineers 'don't put business logic in the Presenter/ViewModel' work as a long-term solution?Because it's a social convention with no automatic enforcement, and it competes against real, felt pressure - it's genuinely faster in the moment to add one inline check than to create and test a new domain class. Conventions without tooling or review enforcement tend to erode gradually as more people touch the codebase under time pressure, especially once one exception is made and becomes precedent.
- What's a concrete way to enforce this boundary at compile time rather than just by convention?Use module-level dependency rules, such as a multi-module setup where the ViewModel module has no dependency edge to the persistence/network module, only to a domain-layer interface, or architecture-testing libraries like ArchUnit that assert package-level import constraints and fail the build if violated. This turns a reviewable-but-missable convention into a build failure.
- Is it ever acceptable for a Presenter or ViewModel to contain a small conditional that looks like business logic?Yes, for purely presentation-specific decisions - showing a red icon if an error code is non-null is UI-state mapping, not a business rule, and belongs in the coordination layer. The distinction is whether the logic would need to be identical on a different platform or a non-UI consumer, like a backend job; if so, it's domain logic and belongs outside the coordination layer.
It's like a receptionist, the coordination layer, who's supposed to just route calls to the right department, but because they're already on the phone, they start answering technical questions themselves instead of transferring the call - each answer given is small and convenient in the moment, but over time the receptionist has quietly become an unofficial, untrained expert desk nobody reviewed for that role.
saying these in an interview costs you the question
- thinks fat Controller/Presenter/ViewModel are unrelated, unconnected problems
- has no answer for why conventions alone fail to prevent this
- can't name any concrete enforcement mechanism beyond 'code review'
- can't distinguish a UI-state decision from an actual business rule
- assumes this is only a Controller (MVC-specific) problem