Here is a shorter version that assumes the reader knows basic software engineering, but not DDD, Effect, XState, or TLA+.
This design uses the application’s own rules to constrain both the running software and any AI agent that changes it.
The core idea is:
Important rules should exist in code, not only in documentation or prompts.
For example, if the business rule is:
An order cannot ship before payment is complete.
That rule should not depend on a developer or AI agent remembering it. The software should represent and enforce it.
Domain-Driven Design, or DDD, is a way to design software around the real concepts and rules of the business.
For an order system, those concepts might include:
- Order
- Payment
- Refund
- Shipment
- Quantity
- Money
The goal is to make the code use the same concepts and rules that the business uses.
For example:
A refund over $5,000 requires review.
That is a domain rule. It should have a clear place in the model.
DDD helps identify these rules and decide where they belong.
Effect Schema lets the program define what valid data looks like at runtime.
For example:
Quantity must be a positive integer.
The schema can reject:
0
-3
2.5
"five"
and accept:
1
2
10
This matters because TypeScript types alone disappear at runtime. External data can still be wrong.
A useful boundary is:
external input
↓
Effect Schema validation
↓
valid domain data
↓
application code
Inside the codebase, strict TypeScript, lint rules, and module boundaries can make it difficult to create invalid domain values directly.
Effect Schema then protects the runtime boundary.
Valid data is not enough.
An action can use valid data and still happen at the wrong time.
For example, these order changes might be valid:
Draft → Submitted
Draft → Cancelled
Submitted → Cancelled
But this might be invalid:
Cancelled → Submitted
A state machine makes these rules explicit.
XState is one TypeScript library for doing this.
The split is useful:
Effect Schema:
Is this value valid?
State machine:
Is this action valid in the current state?
For example, a SubmitOrder command can be structurally valid but still be rejected because the order is already cancelled.
The application also needs to do real work:
- write to a database
- call an API
- send a message
- reserve inventory
- charge a card
Effect can handle this work while keeping failures and dependencies explicit.
A typical flow might be:
Order is Submitted
↓
request inventory reservation
↓
inventory service responds
↓
InventoryReserved event
↓
state machine moves to the next state
The state machine decides what is allowed.
Effect performs the external work.
Some bugs do not come from invalid values or invalid single steps.
They come from valid operations happening in a bad order.
For example:
pay
refund
ship
cancel
Each operation may be valid by itself.
A race between them can still create an invalid result.
TLA+ can model the system as states and transitions and then check whether any allowed sequence can violate a rule.
For example:
A refunded order must never ship.
TLA+ can explore many possible event orders and concurrency cases to find a path that breaks that rule.
The tools answer different questions:
Effect Schema:
Is this data valid?
State machine:
Is this transition allowed now?
TLA+:
Can any allowed sequence of transitions reach an invalid state?
TLA+ checks the model, not the production code, so the implementation still has to match the model.
The same structure can constrain an AI agent that edits the application.
Suppose the agent wants to change:
src/orders/refund.ts
Before allowing that write, the harness can require the relevant domain context to be loaded.
For example:
Refund rules
Order model
Refund state machine
Related schemas
Relevant tests
Relevant formal properties
If that context is missing or stale, the harness loads the current version before the write.
This gives a simple rule:
Before an agent changes domain code, it must have the rules for that domain in context.
Normal agent systems ask:
Is the agent allowed to write this file?
A stronger system also asks:
Does the agent have the information required to change this file safely?
That means a write can require all of these:
permission
+
relevant domain model
+
current version of that model
Only then does the harness allow the write.
This is useful beyond DDD.
For example:
- database change → require schema and migration rules
- auth change → require permission model
- concurrency change → require state or protocol model
- API change → require request and response schemas
For important behavior changes, the agent should update the model first.
Example request:
Refunds above $5,000 must require manual review.
First update the behavior:
RefundRequested
↓ amount <= 5000
RefundApproved
RefundRequested
↓ amount > 5000
AwaitingReview
Then define the rule:
A refund above $5,000 cannot complete without approval.
Then update the implementation.
The process becomes:
requested behavior
↓
domain rule
↓
schema or state-machine change
↓
property checks
↓
implementation
↓
tests
This gives the coding agent a precise target.
A normal coding agent often gets weak feedback such as:
test failed
A domain-aware harness can return something more useful:
Your new transition allows:
Paid
→ RefundRequested
→ Shipped
This violates:
An order with a pending refund cannot ship.
That tells the agent:
- what sequence caused the problem
- which rule it broke
- where to focus the repair
This is especially useful with state-machine tests and TLA+ counterexamples.
The application has several layers:
DDD
defines the business concepts and rules
↓
Effect Schema
defines valid data
↓
State machines
define valid states and transitions
↓
Effect
performs database, network, and other external work
For systems where event order or concurrency matters:
State machines
↓
TLA+ or property checks
check whether allowed behavior can break an invariant
Around this sits the coding-agent harness:
agent requests a change
↓
harness identifies the affected domain
↓
harness loads the current rules and models
↓
agent proposes the change
↓
types, lint, tests, schemas, state checks, and formal checks run
↓
accept or reject
This does not make an AI agent infallible.
It does something more useful:
It makes many bad changes harder to express and easier to detect.
Instead of relying mainly on:
- prompts
- prose documentation
- developer memory
- agent judgment
the system relies more on:
- runtime validation
- types
- explicit state transitions
- tests
- formal properties
- required domain context before writes
The application’s own model becomes part of the safety boundary for the agent that edits it.
That is the central design.