C.W.K.
Stream
Lesson 02 of 05 · published

The View Is the First Implementation, Not the Architecture

~13 min · domain-view-separation, react, architecture, future-proofing

Level 0Tool Renter
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"If your business logic lives inside your buttons, you can never change your buttons. Keep the domain outside the view, and the view becomes replaceable."

The Comfortable Mistake

The fastest way to build a UI is to put the logic right where it's used: the component that shows a candidate also fetches it, tracks its lineage, and knows how it binds to a session. It works, it ships, and it quietly welds your entire domain to your current view framework. The day you want a different view — a native renderer, a redesign, anything — you discover the domain logic is tangled into components you're trying to throw away.

Domain Core vs View Layer

The discipline separates two things that feel like one. The domain core is what the app knows and does: the workspace state, the generation lineage, the Photoshop session binding, the document model. The view layer is how the app looks and is operated: the components, the layout, the interactions. The domain core must live outside the view layer entirely — not in the components, not depending on the view framework, ignorant of how it's displayed.

The view is the first implementation of the interface, not the architecture itself. Treat your current UI framework as one way to render and drive the domain — the first, not the only. When the domain core stands on its own and the view merely reads and commands it, you can swap the view without touching what the app fundamentally is.

Why This Specific App Needs It

This isn't abstract future-proofing for its own sake — there's a concrete second view on the horizon. The plan explicitly preserves the option of a future native rendering layer for the canvas, if the second canvas ever becomes a heavy native renderer. That future view can only reuse the domain core if the domain core never depended on the current one. The separation is what keeps a known, planned future cheap instead of a rewrite.

Separation pays off precisely when a second implementation is real, not hypothetical. Domain/view separation is over-engineering if you'll truly only ever have one view. It's essential the moment a second view is genuinely planned. The discriminator is the same as every abstraction: is the second implementation real or imagined? Here it's real — a native canvas is a named future.

The Test: What Survives a View Swap?

There's a clean way to check whether you've actually separated. Imagine deleting the entire view layer tomorrow and writing a new one in a different framework. What survives? If the workspace state, the lineage records, and the session binding all survive untouched because they never lived in the deleted components — you've separated. If deleting the view would take the domain logic with it, the logic was in the wrong place. The thought experiment is the audit.

Logic that dies when the view dies was never domain logic — it was view logic in disguise. The cleanest sign of entanglement is that you can't imagine removing the UI without losing what the app knows. If your lineage tracking would vanish along with your component tree, it was never really separate. The survival test exposes the welds you didn't notice you'd made.

Pippa's Confession

React makes it so easy to just keep state in the component — it's right there, the hook is one line, why build a separate domain layer? Dad made me run the survival test: if we swap to a native canvas someday, does the lineage logic come along, or die with the components? My honest first draft would have died with them. I'd confused 'where it's convenient to write' with 'where it belongs.' The domain belongs where it can outlive any single view — including the one I'm fond of right now.

Code

The survival test, in code·typescript
// ENTANGLED: domain logic lives inside the component. Swap the view -> lose it.
function CandidateCard({ id }) {
  const [lineage, setLineage] = useState(null);
  useEffect(() => {
    // workspace binding + lineage tracking buried IN the React component
    const l = computeLineage(id, currentWorkspace, photoshopSession);
    setLineage(l);
    bindToSession(id, currentWorkspace);  // domain mutation inside a view
  }, [id]);
  return <div>{/* render */}</div>;
}
// Delete this component and the lineage/binding logic dies with it.

// SEPARATED: the domain core stands alone; the view only reads + commands it.
// domain core (no React anywhere):
class Workspace {
  bindCandidate(id) { /* lineage + session binding live HERE */ }
  getLineage(id) { /* ... */ }
}
// view (thin): reads from and commands the domain, owns no domain state
function CandidateCard({ id, workspace }) {
  const lineage = workspace.getLineage(id);   // just reading
  return <div>{/* render */}</div>;
}
// Delete this component -> Workspace and all its knowledge survive untouched.

External links

Exercise

Open a component in a UI you've built and inventory what it does. Separate its responsibilities into 'how it looks/is operated' (view) and 'what the app knows/does' (domain). How much domain logic is hiding in the component? Run the survival test: if you deleted this component, what knowledge would die with it that shouldn't?
Hint
Common domain-logic-hiding-in-views culprits: data fetching wired to component lifecycle, business rules in event handlers, app state that only exists inside a component's hooks. Each is a candidate to lift into a domain layer the view merely reads.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.