Sniply Blog

MVVM, Micro‑Frontends, and Domain‑Driven Frontend — A Practical Guide

This post gives you crisp mental models and copy‑pasteable patterns for three frequently‑confused topics:

TL;DR: Use MVVM to structure components, DDD to structure the system/domain, and micro‑frontends only when org/scale complexity requires independent deploys.


Quick Compare (When to use what)

ConcernMVVMDDD (frontend)Micro‑Frontends
Primary goalDecouple UI from state/logicAlign codebase with business domains & boundariesScale teams with independent deploys
GranularityComponent/feature levelApp/system architectureApp/organization level
Key benefitsTestable UI logic, clearer componentsLow coupling, shared language, safer changesTeam autonomy, parallel delivery
Typical costExtra layer (ViewModel)Upfront modeling + boundariesRuntime integration, perf, shared deps
Use if…Components mix view & logicFeatures cross‑contaminate domainsMany teams, large app, diff release cycles

MVVM in React (practical)

Model = domain data (entities/value objects).
View = React UI.
ViewModel = state + behaviors the View needs (pure, testable, no DOM).

// /features/todos/viewmodel/useTodosVM.ts
import { useState, useMemo, useEffect } from 'react'
import { fetchTodos, toggleTodo } from '../services/todoApi'

export function useTodosVM() {
  const [items, setItems] = useState([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  async function load() {
    setLoading(true); setError(null)
    try {
      const data = await fetchTodos()
      setItems(data)
    } catch (e) {
      setError('Failed to load')
    } finally {
      setLoading(false)
    }
  }

  async function toggle(id) {
    setItems(xs => xs.map(x => x.id === id ? { ...x, done: !x.done } : x))
    try { await toggleTodo(id) } catch {}
  }

  const stats = useMemo(() => ({
    total: items.length,
    done: items.filter(x => x.done).length,
  }), [items])

  return { items, loading, error, load, toggle, stats }
}
// /features/todos/components/TodoList.tsx (View)
import { useEffect } from 'react'
import { useTodosVM } from '../viewmodel/useTodosVM'

export function TodoList() {
  const vm = useTodosVM()
  useEffect(() => { vm.load() }, [])
  if (vm.loading) return <p>Loading…</p>
  if (vm.error) return <p role="alert">{vm.error}</p>

  return (
    <div>
      <p>{vm.stats.done}/{vm.stats.total} done</p>
      <ul>
        {vm.items.map(t => (
          <li key={t.id}>
            <label>
              <input type="checkbox" checked={t.done} onChange={() => vm.toggle(t.id)} />
              {t.title}
            </label>
          </li>
        ))}
      </ul>
    </div>
  )
}

Why MVVM here works

Tip: A custom hook is a great ViewModel. For shared state, use a store (Zustand/Jotai) and export selectors as VM surface.


Domain‑Driven Frontend (DDD for UI teams)

Goal: mirror business domains and bounded contexts so features don’t leak across layers.

Core ideas

Foldering (Feature‑Sliced/Domain‑First)

src/
  app/                # app shell, routing, providers
  shared/             # cross‑cutting (ui, lib, api, styles, tokens)
  entities/           # reusable domain entities (User, Campaign, Money)
  features/           # user‑visible capabilities (CreateCampaign, SendSMS)
  pages/              # Next.js routes (compose features)
  widgets/            # page sections composed from features/entities

Example: Value Object

// /entities/money/money.ts
export class Money {
  constructor(readonly amount: number, readonly currency: 'EUR'|'USD'|'IRR') {
    if (!Number.isFinite(amount)) throw new Error('Invalid amount')
  }
  add(x: Money) { if (x.currency !== this.currency) throw new Error('Currency mismatch'); return new Money(this.amount + x.amount, this.currency) }
  format() { return new Intl.NumberFormat(undefined, { style:'currency', currency: this.currency }).format(this.amount) }
}

Application Service (use‑case)

// /features/create-campaign/application/createCampaign.ts
import { Money } from '@/entities/money/money'
import { CampaignRepo } from '../infrastructure/CampaignRepo'

export async function createCampaign(input: { title: string; budget: number; currency: 'EUR' }) {
  const budget = new Money(input.budget, 'EUR')
  // business rules…
  return CampaignRepo.create({ title: input.title, budget })
}

Outcome: Separation between domain and delivery (React/UI); safer refactors, clearer ownership.


Micro‑Frontends (MFE)

Split a large UI into independently built & deployed parts. Useful for many teams, different release cycles, or poly‑stacks.

Integration styles

  1. Build‑time: compose artifacts during CI (simpler, less dynamic).
  2. Runtime via Module Federation: load remote bundles at runtime (webpack MF) — most common.
  3. iframes/Web Components: hard isolation; tradeoffs in UX/sharing.

What to share

Minimal Module Federation sketch

// host/next.config.mjs (webpack override)
module.exports = {
  webpack: (config) => {
    const ModuleFederationPlugin = require('webpack').container.ModuleFederationPlugin
    config.plugins.push(new ModuleFederationPlugin({
      name: 'host',
      remotes: { billing: 'billing@https://cdn.example.com/billing/remoteEntry.js' },
      shared: { react: { singleton: true, requiredVersion: false }, 'react-dom': { singleton: true } },
    }))
    return config
  }
}

Routing + Ownership

Pitfalls & mitigations

Use MFEs when organizational complexity dominates. If one team, one repo, don’t start here.


Putting it together

Diagram (text)

[ App Shell ]
   ├── [ Domain A MFE ] -- MVVM inside
   ├── [ Domain B MFE ] -- MVVM inside
   └── [ Shared DS/Auth/Telemetry ]

Decision Guide

  1. Single team, growing app? → Start with DDD foldering + MVVM.
  2. Multiple teams, conflicting releases? → Add MFE for those domains.
  3. Migration: carve out a bounded context → expose via Module Federation → route traffic gradually.

Testing & Observability


Starter Folder Blueprint (Next.js App Router)

src/
  app/
    layout.tsx
    (routes)...
  shared/
    ui/ (design system)
    lib/ (utils)
    api/ (clients)
  entities/
  features/
  widgets/

Cheatsheet


If you want, I can generate a minimal Next.js template that implements:

MVVM, Micro‑Frontends, and Domain‑Driven Frontend — A Practical Guide · Sniply