FTS and DDD: bounded context, ubiquitous language and command guard
The chapter explains where the FTS category coincides with the bounded context from DDD, and where it ends: FTS well describes facts and state transitions, but does not replace the aggregate, repository, and event bus.
Minimal example
категория «Исполнение заказа»
объект Заказ
номер является строкой
«готов к отгрузке» является состоянием «Готов к отгрузке»
морфизм «Готовый заказ можно отгрузить»
если «Готов к отгрузке»
то «Отгрузить заказ разрешено»
What the compiler does
fts check parses category, object, and morphism, checks that the state
«Ready for shipment» is typed, and the morphism condition refers to a fact
actually declared for the object. If the rule in the morphism refers to a non-existent
state, compilation will fail: fts check will return diagnostics before
the code reaches code review. fts prove (or fts certify/fts verify with
context) goes further: it checks that for a specific order instance, the fact
«ready for shipment» is indeed equal to да in the data, and not just declared in
the type.
Category as bounded context
One .fts category is one bounded context, not the entire company’s subject area.
“Order execution” is responsible for transitioning from readiness to shipment resolution.
Payment, accounting, and inventory accounting are other contexts with their own models,
even if they read the same заказы[номер] field from the shared database.
Terms within a category must match the language used by the business. If the warehouse says «confirmed receipt», the model must have «склад подтвердил», not warehouseFlag or status === 3. Ubiquitous language in FTS is not a declaration in a glossary — it is executed: fts test fails if the example does not match the rule written in the same words used by the domain expert.
Morphism as an Allowed State Transition
A morphism in FTS is not an arbitrary function, but a specific allowed transformation of a fact into another fact: “if ready for shipping, then shipping is allowed”. It does not perform shipping and does not move money — it asserts the validity of the transition. The actual transition (a record in the DB, calling a courier service) remains as code outside. This is a direct correspondence to the DDD principle: domain rules are separated from infrastructure effects.
Command guard: why proof is allowed instead of verification in the service
examples/fts/shipment-guard/guard.mjs shows what access looks like
in practice. The shipmentTheorem(orderNumber) function builds the theorem text,
decideShipment compiles it and calls prove:
export function decideShipment(orderNumber, context) {
const source = shipmentTheorem(orderNumber)
try {
const proof = prove(compile(source), context)
return { allowed: true, source, proof }
} catch (error) {
return { allowed: false, source, reason: error.message, diagnostics: error?.diagnostics ?? [] }
}
}
The difference from the regular if (order.readyToShip) ship() is that here the decision is
not a boolean value inside the service, but the result of an independent proof on
the provided data snapshot. Running on a real order model:
node examples/fts/shipment-guard/guard.mjs ЗК-7781
yields "allowed": true together with the path derivation заказы[номер="ЗК-7781"].готов к отгрузке. If the warehouse did not confirm receipt, the same order number with a different
node examples/fts/shipment-guard/guard.mjs ЗК-7781 --blocked
returns "allowed": false and diagnostics FTS_WITNESS_MISMATCH with the exact
path and comparison expected true, got false. The process ends with code 1,
so the guard is suitable for shell, for CI, and for an agent pipeline: neither
agent explanation nor response text can replace this check.
A separate detail: the order number is checked by the regular expression /^[\p{L}\p{N}-]{1,32}$/u before being substituted into the theorem text. Without this check, quotation marks inside the value could break the parsing and alter the theorem’s formulation — guard.test.mjs separately checks that such a number is rejected before compilation.
A certificate or proof does not replace an optimistic lock. Between a data snapshot and a write, the state may change, so the transactional boundary and version check remain the responsibility of the application, not FTS.
Where the Aggregate Boundary Lies
FTS does not store state and does not manage parallel access. The model
describes facts and transition rule; application service collects an aggregate
snapshot, calls prove/verify, and only on success executes the command.
Place .fts next to the bounded context, not in a shared rules folder:
src/order-execution/
domain/
application/
policies/
shipment.fts
shipment.context.schema.json
Version control policy changes as code: a pull request with edit .fts
must show the modified rule, modified examples, and the impact on
generated artifacts.
Practice in the sandbox
Replace the order number in the theorem and context with a non-existent one and look at the diagnostics.
Common Fallacies
FTS_NATURAL_DECLARATION— block starts not withобъект,структура,морфизм,теоремаorутилита». Опечатка вродеPurchase entity` ловится компилятором до валидации типов.FTS_WITNESS_MISMATCH— факт, на который ссылается теорема, не совпадает со значением в context (пример выше с--blocked). Это основной способ, которым command guard отказывает в допуске.- Плохой, но не диагностируемый компилятором антипаттерн: морфизм, который
«доказывает» закон только потому, что он объявлен в
.fts, без реального бизнес-обоснования у эксперта предметной области. Компилятор проверяет типовую согласованность, а не то, верен ли закон в реальности.
Упражнение
Возьмите команду с реальным бизнес-отказом из своего проекта — approve,
refund, publish или deploy. Опишите минимальный факт допуска, один
морфизм и конкретный snapshot, на котором prove должен и не должен
проходить. Отдельно, текстом вне .fts, перечислите технические условия
(лок, идемпотентность запроса, ретраи), которые остаются вне модели.
Чек-лист
- Категория покрывает один bounded context, а не всю систему.
- Имена полей и состояний совпадают с языком предметного эксперта дословно.
- Морфизм описывает переход факта в факт, а не вызывает эффект.
- Command guard использует
prove/certify+verifyна актуальном снимке, а не хранит «уже посчитанный» булев флаг. - Optimistic lock и транзакционная граница остаются в application service.
.ftslies next to bounded context and is reviewed as code.