Architecture for delegation

UI boundaries that keep delegated changes understandable.

9 min read Part 1 of 3

The work that generation leaves behind

In my earlier post about adopting LLM tools, I wrote about spending more time reviewing than coding. Generating a change had become easier. Understanding whether that change belonged in the application was still expensive.

That is the problem behind the architecture I want to describe here. I want to delegate substantial implementation work while keeping the result understandable. I also want validation inside the agent’s working process, not a pile of generated code waiting for a human to check afterward.

An LLM can help build that validation too: an ESLint rule for a concrete dependency constraint, a test for a requirement, or an end-to-end scenario for a critical journey. The architecture should make those checks cheap enough to run, interpret, and act on while the change is still being made.

I cannot give you a measured productivity improvement for this approach. What I can explain is the set of decisions I have opted for, the feedback they make possible, and the extra work they introduce. This is an account of engineering preferences and their consequences, not a claim that a folder structure fixes unreliable generation.

A feature I can reason about in isolation

Consider a cycling app that estimates the work for a planned ride. The rider chooses a duration and a training zone. Using FTP, the rider’s reference power in watts, the app selects a target power and displays estimated mechanical work in kilojoules, or kJ. The rider can then save the workout to their plan.

A zone covers a power range, so the app needs an explicit convention for selecting one target. For this example only, Endurance targets 60% of FTP and Tempo targets 80%. These are illustrative product choices, not a universal zone system or a training recommendation. The ride has one constant target, not intervals.

With an FTP of 250 W, Endurance targets 150 W. A 60-minute ride at that power gives 150 × 60 × 60 ÷ 1,000 = 540 kJ. Halving the duration gives 270 kJ. Selecting Tempo for 60 minutes gives 200 W and 720 kJ. This estimates work at the planned power, not actual ride output or dietary calories.

The feature is small, but it involves several responsibilities: owning the duration and zone draft, calculating target power and work, rendering the result, and saving the workout. If they are mixed together, reviewing the component means reconstructing all of them at once. The estimate itself needs no network request; only persistence is asynchronous.

I prefer an interface small enough that I can describe the feature without introducing the rest of the screen. What are its inputs? What can the person do? What external operations does it require? What result should each operation produce?

That becomes a useful unit of delegation. An agent can change it, another engineer can inspect it, and a test can exercise it without manufacturing unrelated application state.

The same boundary shortens the feedback cycle. A pure decision can be checked without rendering a screen. A controller and view can be exercised with controlled service responses. Dependency rules can reject forbidden imports. The agent can use those failures to correct one slice before moving on, rather than discover every mistake during a final application-wide check.

Four responsibilities, created when needed

I organize the interaction around core decisions, use cases, a controller, and a view. These are responsibilities first. They do not imply four files for every button.

Workout controller
Own duration and zone, derive the estimate, coordinate saving, prepare the view's props.
Core
Calculate target watts and estimated kJ from duration, zone and FTP.
Explicit inputs and units
Use cases
Save workouts through a supplied service and update the plan cache.
I/O, identity and cache behavior
View
Show duration, zone, target watts, estimated kJ and save state.
Props in, user intent out
Arrows represent the controller's dependencies, not a request pipeline. Application setup supplies the concrete services; tests can replace them.

Core owns the zone-to-target convention, input checks, and work calculation. It receives plain values and returns an estimate or an invalid-input result. It does not need component context, a query client, or access to the browser.

Use cases own external operations and their cache behavior. Here, saving writes a workout through a service supplied by the application and updates the rider’s plan cache. Identity scoping and cache correctness belong there. If the host loads FTP through a query, rider identity belongs in that query’s key; TanStack Query documents the relationship between query inputs and keys. The arithmetic does not need a query.

The controller owns the duration and zone draft. It derives the estimate from that draft and the supplied FTP, coordinates saving, and prepares the view’s values and callbacks. It closes the editor only after a successful save and keeps the draft available after failure.

The view renders labelled controls, target power, estimated kJ, validation feedback, and save state. It emits changes and save intent. It does not duplicate the calculation or decide what happens after persistence succeeds.

For this series, the code uses TypeScript and React. The distinction between decisions, operations, coordination, and rendering is useful independently of that syntax.

Decisions that don’t need a component

Here is the calculation I want outside the controller. A watt is a joule per second, so power multiplied by duration in seconds gives joules. Dividing by 1,000 gives kJ. The names keep those units visible.

The function accepts a typed zone and numeric values. The application must parse and validate untrusted input at its boundaries; a TypeScript union is not runtime validation of a network response. The view formats the result for display without changing the calculation’s precision.

This does not need an extensible training engine. The small module earns its place because it names a real domain operation, keeps the target convention in one place, and can be checked independently. Adding intervals or another zone system would be a separate product decision.

The controller connects; the view presents

I do not need to extract a hook solely to make a component look architectural. A small controller can be the component itself. What matters is which work it owns.

This is an abbreviated connection, not a complete implementation. The host supplies a loaded FTP, the hook, and the contracts. The view disables Save for an invalid estimate, disables editing and submission while saving, and shows errors. Changing duration or zone derives a new estimate immediately; there is no loading state for local arithmetic.

The save payload contains the source inputs, not a client-calculated kJ value to trust. The server must validate them, including the FTP snapshot for the rider, and calculate its own result. Handling a changed profile FTP is a separate contract to define, not something this sketch silently resolves.

The mutation owns persistence and cache correctness. The controller owns the screen-specific response to success. I want that division to be consistent across features, so I do not have to rediscover it during every review.

State ownership matters more than filenames

Extracting markup does not help much if the parent still holds every section’s drafts, results, errors, and save handlers. I have made a smaller rendering file, but the feature still needs the whole screen to exist.

My default is to keep state with its lowest shared reader. Server state belongs in the query cache. Unsaved edits belong to the editing unit. Workflow state belongs to the flow owner. Shared fields can use a scoped form context, with each unit subscribing to the slice it needs.

This also means avoiding a second state variable for something I can derive. React’s state-structure guidance discusses the synchronization problems created by redundant and contradictory state. The extra boundary is much less valuable if it just distributes duplicated state across more files.

A screen can compose several units without taking ownership of their individual drafts. A layout view can receive those units as children. I use context when siblings genuinely share state, not as a way to hide an entire application’s dependencies.

Where I stop splitting

A badge that renders props is already a view. A disclosure can keep its open state locally. Focus management and viewport handling are visual behavior; putting them in a domain controller would make ownership less clear.

A section with a domain query or an editable business value earns a controller. A substantial branching rule earns core code. A simple expression can stay where it is readable.

All of this can live inside ordinary app feature folders. Existing providers are usually enough to supply services, identity, routing, and locale. A replaceable interface does not require a workspace package or a new dependency-injection framework.

The cost I am accepting

There are more interfaces to navigate. A change can touch a decision, a controller, and a view. Prop contracts need maintenance. Extracting the wrong rule can create a generic helper whose callers are harder to understand than the original code.

I am willing to pay that cost when it reduces the amount of unrelated context needed to review and verify a feature. I am not willing to pay it just to make every directory symmetrical. A boundary should own something I can name.

This architecture gives me a place to build feedback around generated code. The agent can help write the checks as well as the implementation, but both can share the same misunderstanding. A clean boundary makes validation possible; it does not make the expectations correct.

The next article is Verification that earns trust: how I want ESLint rules, behavioral tests, mutation testing, and end-to-end checks to guide implementation, and how I would challenge the evidence they produce.