Part 1 described what it feels like when a codebase starts resisting change. This part is the opposite. It is the small set of boundaries that make change boring again.

The goal is not to invent a new pattern. It is to keep responsibilities narrow enough that the system stays loose even as features pile up, migrations happen, and teams rotate.

In the previous piece, this structure was referred to once as SCALES. The name is not the important part. The boundaries are.

The Shape in One Diagram

There are four core pieces. Several are reference objects by design. One is intentionally value-only.

View ──renders──▶ ViewContext ──closures/events──▶ AppContext ──calls──▶ AppServices
▲                     │                              │                 │
│                     │                              │                 │
└──re-renders◀────────┴────────────observes──────────┴──updates──▶  AppState (values)

Read it left to right:

  • Views render and emit intent.
  • ViewContext is the view’s contract: prepared data plus interaction points.
  • AppContext wires and orchestrates: it connects interaction points to implementations and is the only place that mutates state.
  • AppServices do work and side effects, returning results.
  • AppState stores value data only, observable by the UI.

That’s the whole thing.

The constraints are where it becomes useful.

AppState Is Value Data Only

AppState is intentionally boring. It holds values, not behavior.

It can be a reference object (often it is, because it is observed), but what matters is what it contains:

  • identifiers, enums, structs, arrays, dictionaries of value types
  • derived, serializable domain state
  • nothing from the reference layer

What does not go in AppState:

  • services
  • stores
  • persistence contexts
  • coordinators or orchestrators
  • message channels
  • view contexts
  • anything that “does work” or “knows how” to do work

This rule is not aesthetic. It is defensive. It prevents state from becoming a dumping ground for hidden coupling, and it keeps debugging honest. If state is values only, you can print it, snapshot it, test it, and reason about it without dragging half the runtime into scope.

AppServices Do Work, Not Orchestration

Services are where side effects live:

  • persistence
  • exports
  • purchases
  • network calls
  • file I/O
  • heavyweight transforms

A service method returns success or failure, or a result. It does not reach into AppState and mutate anything directly.

That line matters because services are where complexity hides. If services can mutate state, you lose the ability to answer a simple question during debugging: who changed this?

In this setup, the answer is always the same. AppContext did, based on a service result.

It also keeps services testable. A service can be exercised as a contract without recreating UI wiring.

AppContext Owns Mutation and Wiring

AppContext is the single root orchestrator. It owns:

  • AppState
  • AppServices
  • the message channel, when used
  • the wiring that turns UI intent into state updates

AppContext is where actions are executed. It is also where boundaries are enforced. If something needs to mutate AppState, it routes through AppContext. If something needs to coordinate multiple services and then update state, it routes through AppContext.

This concentrates responsibility in one place on purpose. The alternative is spreading orchestration across view models, services, and helpers until nobody can find the decision points.

A good mental model is that AppContext is not “business logic.” It is the conductor. It sequences work, interprets results, and updates state.

ViewContext Is a Contract, Not a ViewModel

“ViewModel” is a loaded term. Different teams mean different things by it. Some mean presentation state. Some mean an object that owns dependencies. Some mean the place where everything goes.

ViewContext is named to avoid that ambiguity.

A ViewContext is the whole context a view needs to function, but packaged as a contract that does not expose other layers. It typically contains:

  • prepared presentation data (already shaped for the view)
  • UI-facing subscriptions or derived state, when needed
  • interaction points as closure properties

ViewContext is often a reference object (@Observable or ObservableObject) because it has identity, manages UI-facing lifecycles, and may own subscriptions. That is fine. The boundary is that ViewContext must not retain the architecture root or the service layer.

It should not hold AppContext, services, stores, or persistence contexts. It should hold what the view needs, and nothing that would let the view reach around the contract.

The result is a view that stays simple. It renders. It invokes interaction points. It does not learn your storage or orchestration story.

One-Way Flow, With Explicit Wiring

The data flow is intentionally one-way:

  1. The view invokes an interaction point on its ViewContext (a closure property like showPDF or submit).
  2. AppContext wires those interaction points by assigning implementations during ViewContext construction.
  3. The wired implementation performs orchestration, calls services as needed, and interprets results.
  4. AppContext updates AppState from those results.
  5. Views re-render from observable state changes.

This might sound familiar, but the practical difference is explicit wiring and ownership. Interaction points live on the ViewContext as a contract. Orchestration and state mutation live in one place, which keeps the system predictable.

Why This Stays Flexible

This structure makes implementation details easier to replace because dependencies do not leak upward.

If persistence changes, it changes behind AppServices. Views do not care. ViewContexts do not care. State does not care.

If orchestration changes, it changes inside AppContext. Views still invoke the same interaction points.

If a feature needs to become more independent, it can use a message channel boundary rather than introducing new direct dependencies.

The system stays loose because contracts are small and ownership is explicit.

The next piece zooms in on that last point: how a small, scoped message channel acts as a pressure relief valve for independent components, without turning the codebase into a global pubsub mess.