For years, I’ve heard the same statement repeated in iOS teams, meetups, code reviews, and interviews:

“MVVM-C doesn’t scale.”

It’s usually said with confidence. Often by smart engineers. Almost always after a painful experience.

Here’s the uncomfortable, experience-backed truth:

MVVM-C can scale extremely well — when applied with discipline. What usually doesn’t scale is an undisciplined interpretation of it.

This edition of Swift in Practice is not about defending a pattern out of ideology. It’s about understanding why it fails in real teams — and how to apply it in a way that survives growth, refactors, and organizational change.

This is not academic theory. It’s based on shipping production apps, migrating large codebases, mentoring engineers, and watching systems age.

Scope note: This article assumes medium-to-large SwiftUI codebases, developed over multiple years, by multiple engineers or teams. If you’re building a small app, a solo product, or a short-lived prototype, MVVM-C may be unnecessary overhead.

What “scale” actually means

Most architectural debates fail because teams argue about different definitions of scale.

Scale is not:

  • Number of screens
  • Number of ViewModels
  • Number of files or folders

Scale is:

  • Number of business flows
  • Number of engineers touching the same code
  • Ability to change behavior without rewriting UI
  • Ability to remove features without destabilizing others
Architectures collapse when responsibilities blur under growth. MVVM-C exists precisely to prevent that.

Why MVVM-C gets a bad reputation

MVVM-C emerged in the UIKit era as a pragmatic response to Massive View Controllers.

It worked:

  • Responsibilities were separated
  • Navigation was extracted
  • Testing became feasible

Then SwiftUI arrived.

Instead of rethinking the model, many teams ported it verbatim:

  • Same Coordinator responsibilities
  • Same ownership assumptions
  • Same lifecycle thinking

The result was predictable:

  • Bloated Coordinators
  • Navigation logic leaking everywhere
  • Implicit global state
  • Fragile, feature-entangled flows

The conclusion was convenient:

“The pattern doesn’t scale.” But the pattern was never the real issue.

The real failure modes

The God Coordinator

A single Coordinator that:

  • Knows every screen
  • Controls every flow
  • Accumulates logic indefinitely
  • Becomes a merge-conflict magnet
This is not architecture. It’s a Massive View Controller wearing a different costume.

Rule:

👉 One Coordinator represents one business flow, not the entire app.

Coordinators owning business logic

Coordinators should orchestrate where the app goes — not why something is allowed.

When business rules move into Coordinators:

  • Logic gets duplicated
  • Testing becomes convoluted
  • Responsibilities blur
Business rules belong in ViewModels or domain use cases.
Coordinators only translate intent into navigation.
In real systems, some routing is informed by business constraints (compliance, entitlements, experiments). Coordinators may react to outcomes — but should not derive the rules themselves.

Tight coupling through concrete dependencies

When ViewModels depend on concrete Coordinators:

  • Tests become brittle
  • Reuse becomes impossible
  • Features lock together
Abstractions are not optional at scale — they are survival tools.

No real feature boundaries

MVVM-C alone does not create a scalable system.

Without clear feature ownership:

  • Teams collide
  • Knowledge spreads uncontrollably
  • Refactors become high-risk events
A scalable structure is feature-first, not file-first.
Each feature owns its flows, views, view models, and domain logic.
This is where MVVM-C compounds in value.

Modeling Coordinators in SwiftUI

SwiftUI’s declarative nature requires a different approach than UIKit.

Navigation state becomes explicit, observable, and flow-scoped.

Each Coordinator:

  • Owns a NavigationPath
  • Exposes semantic destinations
  • Represents one business flow
SwiftUI navigation APIs will continue to evolve. Coordinators model intent and flow — not a specific Apple API.

Integration with Views

Testability that survives growth

In real systems, coordinator tests assert semantic destinations, not raw path counts.

Cross-cutting concerns inside Coordinators

When MVVM-C is not enough

MVVM-C may struggle when:

  • Multiple teams enforce different paradigms
  • Strict unidirectional data flow is required
  • Global state coordination dominates
  • Time-travel debugging is critical
More prescriptive architectures (TCA, Redux-style) may scale teams better. This is an informed choice, not a failure.

A Human Ending

Architecture is not about patterns. It’s about reducing fear.

I’ve inherited MVVM-C codebases that were painful to evolve — and others that felt calm, predictable, and resilient.

The difference was never the pattern itself: it was whether boundaries were protected early, and whether teams treated architecture as a living system.

A well-applied MVVM-C:

  • Makes complexity explicit
  • Enables testing and reasoning
  • Supports long-term team velocity
And that is what scaling really means.

Written for Swift in Practice — where we discuss the architectural decisions, trade-offs, and lessons learned from building real iOS products over time.

If this resonated with you, follow or subscribe for more pragmatic Swift engineering insights.

👋 Author

Bruno Valente Pimentel — Senior iOS Architect & Consultant 25+ years building at the intersection of code, creativity, and communication.

☕ Enjoying Swift in Practice? It's free and reader-supported. If an edition saved you some debugging time, you can buy me a coffee: https://buymeacoffee.com/swiftinpractice

Originally published as a Swift in Practice edition on LinkedIn. Comments and discussion happen there.