Learn how to implement a practical, strict MVVM architecture in C++ to build clean, testable, and scalable cross-platform UI systems.

Cross-platform C++ applications often live in an uncomfortable middle ground. The business logic needs to be portable, performant, and independent of the operating system, while the user interface must adapt to completely different platforms, frameworks, and interaction models. Without a strong architectural boundary, UI code quickly becomes tangled with business rules, threading, networking, device APIs, and application state. Strict Model-View-ViewModel (MVVM) provides a disciplined way to separate those concerns. But implementing MVVM rigorously in C++ requires more than naming a few classes `Model`, `View`, and `ViewModel`. The architecture needs explicit ownership rules, observable state, clear data flow, platform-independent business logic, predictable asynchronous behavior, and interfaces that can be tested without launching the UI. Done correctly, strict MVVM allows teams to share core application logic across Windows, macOS, Linux, mobile, or embedded targets while letting each platform maintain an appropriate user experience.
Cross-platform applications have two competing requirements.
The first is shared logic.
The second is platform-specific experience.
For example:
Shared Core
│
┌────────────┼────────────┐
▼ ▼ ▼
Windows macOS Linux
View View ViewThe business rules should not need to be rewritten simply because the interface changes.
Without architectural boundaries, applications often evolve into:
UI
│
├── Business Logic
├── Database
├── Networking
├── Threading
└── Platform APIsNow every platform becomes dependent on the same tangled implementation.
MVVM provides a cleaner separation:
View
↓
ViewModel
↓
Model / ServicesThe View can change while the underlying application logic remains largely reusable.
MVVM is sometimes implemented loosely.
A developer might create a ViewModel but still place:
Business logic in the View
Database calls in UI event handlers
Platform APIs in ViewModels
Direct widget manipulation in ViewModels
That is not strict MVVM.
A stricter architecture establishes clear rules:
Responsible for:
Rendering
User interaction
Binding
Platform-specific presentation
Responsible for:
Presentation state
Commands
Validation orchestration
Calling application services
Transforming domain data into UI-friendly state
Responsible for:
Business rules
Domain entities
Persistence
Networking
Core application behavior
The dependency direction should remain predictable.
View
↓
ViewModel
↓
Application Services
↓
Domain / InfrastructureThe domain should not depend on the View.
A practical enterprise architecture can be divided into:
┌─────────────────────────────┐
│ View │
│ UI / Binding / Interaction │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ ViewModel │
│ State / Commands / Mapping │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Model / Services │
│ Domain / Data / Networking │
└─────────────────────────────┘The ViewModel becomes the boundary between the user interface and the application.
This is especially useful in C++ because it keeps platform-specific UI frameworks away from core business code.
A strict MVVM architecture benefits from predictable data flow.
A typical interaction looks like:
User
↓
View
↓
Command
↓
ViewModel
↓
Service
↓
Model
↓
ViewModel State
↓
ViewThe View does not directly modify business objects.
Instead, it expresses intent.
For example:
"The user selected account 42."
The ViewModel decides what that means.
This creates a cleaner separation between:
What the user did
and:
What the application should do about it.
The Model should represent the application's actual domain.
For example:
struct Account
{
int id;
std::string name;
double balance;
};The model should not know about:
Buttons
Windows
Screens
Dialogs
Widgets
UI eventsInstead, it represents application concepts.
For more complex applications, separate the domain from infrastructure:
Domain
├── Entities
├── Value Objects
└── Business Rules
Infrastructure
├── Database
├── HTTP
├── Files
└── Platform APIsThis makes the core easier to test and reuse.
This is one of the most important distinctions.
The ViewModel should coordinate application behavior, but it should not become a giant replacement for the domain layer.
Bad:
void AccountViewModel::transferMoney(...)
{
if (balance >= amount &&
account.isActive() &&
destination.isValid())
{
// dozens of business rules...
}
}A better approach is:
void AccountViewModel::transferMoney(...)
{
transferService_->transfer(sourceId, destinationId, amount);
}The business rules belong in the domain or application service.
The ViewModel translates user intent into application operations.
A ViewModel should expose information in a form the UI can consume easily.
For example:
class AccountViewModel
{
public:
std::string accountName() const;
std::string formattedBalance() const;
void refresh();
void transfer(double amount);
};The ViewModel may transform:
Domain Value
↓
Presentation ValueFor example:
10250.50could become:
"$10,250.50"The View should not need to understand business formatting rules or data transformation.
A strict View should focus on presentation.
It should not contain:
Database Queries
Business Rules
Network Requests
Domain DecisionsInstead:
View
├── Bind Text
├── Bind State
└── Trigger CommandsThe View observes ViewModel state.
When the state changes:
ViewModel
↓
State Changed
↓
View UpdatesThis creates a predictable UI lifecycle.
User actions should be represented explicitly.
Instead of allowing the View to directly call a service:
Button
↓
PaymentServiceuse:
Button
↓
PayCommand
↓
ViewModel
↓
PaymentServiceCommands can expose:
Execute
CanExecute
Progress
Error state
This becomes especially useful for asynchronous operations.
Conceptually:
class Command
{
public:
virtual bool canExecute() const = 0;
virtual void execute() = 0;
};A View can bind to the command without understanding the underlying business operation.
This allows the ViewModel to control whether an action is currently valid.
For example:
Form Invalid
↓
SubmitCommand.canExecute()
↓
false
↓
Button DisabledReactive state is central to modern MVVM.
A ViewModel might expose:
Loading
Data
ErrorInstead of scattering independent flags throughout the UI, model the state deliberately.
For example:
enum class LoadState
{
Idle,
Loading,
Loaded,
Failed
};Then:
struct AccountState
{
LoadState status;
std::vector<Account> accounts;
std::string errorMessage;
};The View observes state changes rather than manually coordinating multiple variables.
The exact implementation depends on the UI framework.
The architecture might use:
Signals and slots
Observers
Reactive streams
Callbacks
Property wrappers
The important principle is not the specific mechanism.
It is the dependency direction:
ViewModel State
↓
Observer / Binding
↓
ViewAvoid making the ViewModel dependent on concrete UI widgets.
C++ makes ownership an architectural concern.
A ViewModel should not create every service it needs internally.
Avoid:
AccountViewModel::AccountViewModel()
{
database_ = std::make_unique<Database>();
api_ = std::make_unique<ApiClient>();
}This makes testing harder.
Prefer dependency injection:
AccountViewModel(
AccountService& service,
Logger& logger);Now production code can provide real implementations while tests provide fakes.
For each major object, determine:
Who creates it?
Who owns it?
How long does it live?
Who can access it?
A typical application might use:
Application
↓
Service Container
↓
Services
↓
ViewModelsThe exact mechanism can vary.
The principle is to make ownership explicit rather than relying on accidental object lifetimes.
Cross-platform applications frequently perform:
Network requests
Database operations
File processing
Device communication
These operations should not block the UI thread.
A typical flow is:
View
↓
ViewModel
↓
Async Service
↓
Background Thread
↓
Result
↓
UI State UpdateThe ViewModel should expose states such as:
Idle
Loading
Success
ErrorThe UI then reacts to state rather than managing threads directly.
A common C++ UI failure is updating UI-observed state from the wrong thread.
For example:
Worker Thread
↓
ViewModel State
↓
UIIf the UI framework requires updates on its main thread, the state notification must be dispatched correctly.
The architecture should make this boundary explicit:
Background Work
↓
Result
↓
UI Thread Dispatcher
↓
ViewModel StateThis avoids race conditions and difficult-to-reproduce crashes.
One of the biggest benefits of strict MVVM is separating platform concerns.
Suppose the application needs:
File selection
Notifications
Bluetooth
Secure storage
Camera
The ViewModel should depend on an abstraction:
class SecureStorage
{
public:
virtual ~SecureStorage() = default;
virtual std::string read(std::string_view key) = 0;
virtual void write(
std::string_view key,
std::string_view value) = 0;
};Platform implementations can then provide:
SecureStorage
│
┌────┼─────┐
▼ ▼ ▼
Win macOS LinuxThe shared application logic remains portable.
This:
#ifdef _WIN32
// Windows API
#endifinside a ViewModel is usually a warning sign.
Prefer:
ViewModel
↓
Platform Interface
↓
Windows ImplementationThis keeps the ViewModel portable and testable.
Enterprise applications need predictable error behavior.
Do not expose low-level infrastructure errors directly to the UI.
For example:
HTTP 503is not necessarily useful to the user.
The application layer can translate it into:
"Service temporarily unavailable. Please try again."The ViewModel can then expose:
Error State
+
User-Friendly Message
+
Retry CommandThis creates a better boundary between infrastructure and presentation.
One of the biggest advantages of MVVM is testability.
You should be able to test:
ViewModel
↓
Fake Service
↓
Expected Statewithout launching the UI.
For example:
TEST(AccountViewModel, LoadsAccounts)
{
FakeAccountService service;
AccountViewModel vm(service);
vm.refresh();
EXPECT_EQ(vm.accounts().size(), 3);
}The exact test framework does not matter.
The important property is that the test does not require:
A window
A real database
A real network
A specific operating system
Good ViewModel tests should focus on behavior.
Examples:
When login succeeds
→ authenticated state is exposed
When login fails
→ error state is exposed
When request is loading
→ submit action is disabled
When data refreshes
→ updated state is publishedThis gives you a stable test suite even when the UI changes.
A large application should not become one enormous ViewModel hierarchy.
Organize around business domains.
For example:
Application
│
├── Accounts
│ ├── Views
│ ├── ViewModels
│ └── Services
│
├── Orders
│ ├── Views
│ ├── ViewModels
│ └── Services
│
└── Reporting
├── Views
├── ViewModels
└── ServicesThis makes ownership clearer.
A ViewModel should represent a meaningful presentation context.
Avoid:
MainViewModel
├── Login
├── Payments
├── Reports
├── Settings
├── Users
└── Everything ElsePrefer:
LoginViewModel
PaymentViewModel
ReportViewModel
SettingsViewModelSmall, focused ViewModels are easier to test and maintain.
A giant ViewModel becomes a new form of spaghetti architecture.
UI code should not decide core business rules.
The ViewModel should coordinate presentation, not become the entire application.
Avoid storing pointers to concrete buttons, labels, windows, or screens inside ViewModels.
Hard-coded service creation makes testing and replacement difficult.
Asynchronous C++ code can easily create race conditions if UI state updates are not carefully controlled.
Global state makes application behavior difficult to reason about.
The architecture should remain understandable. A complicated reactive framework is not automatically better.
A scalable architecture might look like:
Platform UI
┌───────┼───────┐
▼ ▼ ▼
Windows macOS Linux
│ │ │
└───────┼───────┘
▼
ViewModels
│
┌───────┼───────┐
▼ ▼ ▼
Commands State Presentation
│
▼
Application Layer
│
┌────────┼────────┐
▼ ▼ ▼
Domain Services Policies
│ │
└────────┼────────┘
▼
Infrastructure
┌────────┼────────┐
▼ ▼ ▼
Database HTTP Platform APIsThe critical dependency direction is:
UI
↓
ViewModel
↓
Application
↓
Domain
↓
InfrastructureCore logic should not depend on the UI framework.
A complete rewrite is rarely necessary.
Find code where UI classes directly perform:
Database operations
Network requests
Business calculations
File operations
Move application operations into testable service interfaces.
UI Code
↓
Service Interface
↓
ImplementationCreate ViewModels around major user workflows.
Move:
Loading state
Validation state
Error state
Selection state
into the ViewModel.
Convert UI event handlers into explicit application intents.
Choose an appropriate notification mechanism for the UI framework.
Move operating-system-specific behavior behind interfaces.
Test critical workflows without the UI.
Gradually eliminate:
View → Database
View → Network
View → Business LogicDocument and review dependency rules.
Architecture only works when the team consistently follows it.
Strict MVVM should produce measurable improvements.
More logic tested without UI
Faster test execution
Fewer integration-only tests
Smaller ViewModels
Fewer UI dependencies
Clearer service boundaries
More shared code
Fewer platform-specific branches
Fewer threading issues
More predictable state transitions
Easier onboarding
Clearer ownership
Safer refactoring
The architecture should make everyday development easier.
Before adopting strict MVVM, engineering teams should ask:
Which application logic needs to be shared across platforms?
Where is the current UI tightly coupled to business logic?
Which platform APIs need abstraction?
How will ViewModel state be observed safely across threads?
Can our ViewModels be tested without launching the UI?
Who owns service lifetimes and dependencies?
How will the architecture scale as the application grows?
Most importantly:
Are we implementing MVVM to create clear boundaries—or simply adding ViewModel classes to an already coupled codebase?
The difference is significant.
Strict MVVM in cross-platform C++ is less about a particular framework and more about enforcing a disciplined flow of responsibility.
A healthy architecture looks like:
User
↓
View
↓
Command
↓
ViewModel
↓
Application Service
↓
Domain
↓
InfrastructureThe result flows back in the opposite direction:
Infrastructure
↓
Domain / Service
↓
ViewModel State
↓
Binding / Observer
↓
ViewThe View focuses on presentation.
The ViewModel manages presentation state and user intent.
The application layer coordinates use cases.
The domain owns business rules.
Infrastructure handles databases, networking, files, and platform services.
That separation is particularly valuable in C++ because platform boundaries, memory ownership, threading, and native APIs can otherwise leak into every layer of the application.
The biggest benefit is not simply cleaner code.
It is freedom to change one part without destabilizing everything else.
You can redesign the Windows interface without rewriting business rules.
You can introduce a macOS frontend without duplicating application logic.
You can replace a networking implementation without rewriting the View.
You can test complex workflows without opening a window.
You can move platform services behind new implementations without changing the core application.
That is the real power of strict MVVM.
The goal is not to make every C++ class fit neatly into Model, View, or ViewModel. The goal is to create boundaries strong enough that UI, business logic, infrastructure, and platform code can evolve independently.
For cross-platform applications expected to survive years of development, that independence becomes increasingly valuable.
The architecture ultimately becomes:
Platform-specific where experience matters.
Platform-independent where business logic matters.
Explicit where dependencies matter.
Reactive where state changes.
Asynchronous where work is expensive.
Testable where correctness matters.
And when those principles are applied consistently, C++ can provide something many cross-platform teams struggle to achieve:
One shared application core with platform-appropriate experiences on top.
That is what makes strict MVVM more than a design pattern.
It becomes an architectural strategy for keeping complex cross-platform applications understandable, testable, and adaptable as they grow.
We build custom software, mobile apps, and web platforms for startups and enterprises.



Their team became an extension of ours — within months they'd rebuilt our entire product experience from the ground up.
