Build the React SPA so that AI coding agents can change implementation code often without changing product rules by accident.
Separate stable product rules from replaceable implementation code.
Treat changes to the stable part as exceptional.
Use three main areas:
src/
core/
domain/
schemas/
errors/
services/
use-cases/
implementation/
api/
storage/
services/
layers/
app/
runtime/
hooks/
components/
routes/
core/ defines what the system means.
Put these items in core/:
- domain types
- Effect schemas
- data constraints
- product rules
- domain errors
- service contracts
- business use cases
Example:
UserId
User
InvalidUser
UserRepository contract
createUser()
Do not put React code, browser APIs, storage code, or HTTP code in core/.
implementation/ defines how the system performs external work.
Put these items here:
- HTTP clients
- API adapters
- storage adapters
- browser integrations
- service implementations
- Effect Layers
Implementation code can change often.
It must continue to satisfy the contracts in core/.
app/ connects the system to React.
Put these items here:
- React components
- routes
- hooks
- application startup
- Effect runtime setup
- production Layer assembly
Use one-way dependencies.
core
↑
implementation
↑
app
More precisely:
implementation -> core
app -> core
app -> implementation
Never allow:
core -> implementation
core -> app
core/ must not know about React, fetch, local storage, IndexedDB, analytics tools, or other external systems.
Effect works well with this structure.
Use Effect Schema for domain data and constraints.
Use Effect service definitions for dependencies.
Put the service contract in core/.
Put the service implementation and Layer in implementation/.
Example:
core/services/Auth.ts
↓
implementation/auth/AuthHttp.ts
↓
implementation/layers/Production.ts
↓
app/runtime.ts
A test can replace:
AuthHttp
with:
AuthTest
The business use case does not change.
Give agents different authority in different folders.
For core/:
Treat this directory as a stable product contract. Do not change schemas, constraints, errors, service contracts, or business rules unless the task explicitly requires a contract change.
For implementation/:
You can refactor this directory freely. All changes must continue to satisfy the contracts and tests in
core/.
This turns the folder structure into a change-control system.
Do not depend on folder names alone.
Enforce the boundary with code and repository rules.
Use:
- import-boundary lint rules
- tests for
core/ - contract tests
- schema fixtures where useful
AGENTS.mdfiles with folder-specific rules- CI checks for unexpected
core/**changes - CODEOWNERS or extra review for
core/**
A change to implementation/** is normal.
A change to core/** requires explicit intent.
Use separate packages if agents still cross the folder boundary:
packages/
core/
web/
Expose only approved items from the core package.
Package boundaries provide stronger control than folder rules.
Do this only if the added build and package structure gives enough value.
Use this test:
Does this code describe the product, or does it describe one way to implement the product?
Product meaning belongs in core/.
Implementation choices do not.
Good core/ examples:
OrderId
Order
CheckoutError
CanCancelOrder
InventoryService contract
submitOrder()
Bad core/ examples:
fetchJson()
useDebounce()
queryClient
Axios configuration
localStorage wrapper
React context
Keep core/ small.
Do not turn it into a general shared/ directory.
AI agents work better when a task has a fixed contract.
This task is relatively safe:
Implement this service so that it satisfies this existing contract.
This task is less safe:
Change authentication.
Stable contracts reduce the number of decisions that an agent must make.
They also make unwanted changes easier to detect.
The code diff becomes useful evidence:
implementation changed -> expected
core changed -> inspect why
This design combines three established ideas.
Define business rules and contracts separately from external systems.
Treat schemas, contracts, tests, and design rules as more stable than generated implementation code.
Put rules in the repository.
Use tests, lint rules, folder instructions, and CI to enforce them.
Do not depend only on prompts.
HumanLayer advocates much of the spec-first and contract-first approach.
Recent agent-development guidance also favors repository rules, scoped agent instructions, tests, and machine-enforced boundaries.
The additional step here is explicit:
Use the source tree to encode how much authority an AI agent has to change each part of the system.
If the project uses Effect v3, pin that version.
Tell agents to use Effect v3 APIs and documentation.
Do not let an agent silently migrate code to another major Effect version while doing an unrelated task.
A framework version change is a contract-level change.
Treat it as such.
Use this model:
core = what the system means
implementation = how the system performs external work
app = how React runs and presents the system
Then apply this policy:
Protect the core.
Replace the implementation.
Compose everything in the app.
The main purpose is not code organization.
The purpose is controlled change.
The architecture must make accidental changes to product meaning difficult, visible, and reviewable.