alchemy-domain
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuilding with @alchemy.run/domain
使用 @alchemy.run/domain 构建
@alchemy.run/domainDocs: https://alchemy.run/domain, with every page listed in https://alchemy.run/llms.txt. Every section here and in the reference files ends with a More link to the docs page that covers it in depth. A complete reference project: in the alchemy repo (a bank with a foldkit app).
examples/cloudflare-domain@alchemy.run/domain文档:https://alchemy.run/domain,其中每个页面都列于 https://alchemy.run/llms.txt。此处及参考文件中的每一节末尾都有一个“更多”链接,指向深入介绍该主题的文档页面。完整参考项目:alchemy repo 中的 (一个带有 foldkit 应用的银行应用)。
examples/cloudflare-domainWorkflow
工作流程
Work in this order. Each step has a reference file with templates.
- Break the requirements down into building blocks. See references/modeling.md.
- Lay out the monorepo: a domain package, a server package, a web package, with one kind-suffixed file per block. See references/layout.md.
- Write each block from its template. See references/building-blocks.md.
- Specify behavior with stories before and while you implement it. See references/testing.md.
- Host the model on Cloudflare from the server package, export its traces, and deploy with one alchemy stack. See references/hosting.md.
- Drive it from foldkit in the web package. See references/frontend.md.
More: Tutorial, part 1.
按以下顺序执行。每个步骤都有一个包含模板的参考文件。
- 将需求拆解为 building blocks。参见 references/modeling.md。
- 搭建 monorepo 布局:一个 domain package、一个 server package 和一个 web package,每个 block 使用一个带 kind 后缀的文件。参见 references/layout.md。
- 根据模板编写每个 block。参见 references/building-blocks.md。
- 在实现前后通过 stories 明确行为。参见 references/testing.md。
- 在 server package 中将模型托管到 Cloudflare,导出其 traces,并使用一个 alchemy stack 完成部署。参见 references/hosting.md。
- 在 web package 中通过 foldkit 驱动模型。参见 references/frontend.md。
更多:教程,第 1 部分。
From requirements to building blocks
从需求到 building blocks
Read the requirements as a story of facts, and name each kind of thing you find:
- A fact the business records, named in the past tense (): an Event.
MoneyWithdrawn - A request to change something, named (
<Verb><Noun>): a Command.WithdrawMoney - A way a request is refused (): a Rejection.
InsufficientFunds - A thing whose rules must hold in one transaction (one account's balance): an Aggregate. It decides commands against its state.
- A question someone asks (balance, statement, dashboard): a View, keyed by what it's about.
- "Whenever X happens, do Y": a Policy.
- A process with steps, waits and undo (move money: debit, credit, refund on failure): a Saga, started by a policy.
- Something outside the model you call (fraud scoring, payments): an External System.
- Writes to a store outside the model (a ledger table): a Projection. An ordered, readable log per key (account cards): a Stream that a projection appends to.
- A question across keys, answered from that store (an account's ledger, paged; large withdrawals across all accounts, largest first): a Query. Never a hand-written route.
- Who can do and see what: an Api listing commands and reads, with middleware (sessions, MFA) and each block authorizing itself: a command in , a view or stream in its Layer's
decide, a query in its handler.validate
A rule that spans aggregates never becomes a cross-aggregate transaction. Give the invariant its own aggregate, keyed by what it protects ( keyed by username). Or reserve first and commit after, in a policy or saga, with a deadline for anything that waits.
UsernameClaim将需求视为由事实组成的叙事,并为你发现的每类事物命名:
- 业务记录的一项事实,使用过去时命名():一个 Event。
MoneyWithdrawn - 更改某事的一项请求,命名为 (
<Verb><Noun>):一个 Command。WithdrawMoney - 拒绝请求的一种方式():一个 Rejection。
InsufficientFunds - 其规则必须在单个 transaction 中成立的事物(例如一个账户的余额):一个 Aggregate。它根据自身 state 决策 commands。
- 某人提出的一个问题(余额、对账单、仪表盘):一个 View,按其主题设置 key。
- “每当 X 发生,就执行 Y”:一个 Policy。
- 包含步骤、等待和撤销操作的过程(转移资金:扣款、入账,失败时退款):一个 Saga,由 policy 启动。
- 模型之外所调用的系统(欺诈评分、支付):一个 External System。
- 向模型之外的 store 写入数据(账本表):一个 Projection。按 key 存储的有序可读日志(账户卡片):一个 Stream,由 projection 向其追加内容。
- 跨 key 提出的问题,并从该 store 获得答案(分页查询某个账户的账本;按金额从大到小查询所有账户的大额取款):一个 Query。绝不编写手写 route。
- 谁可以执行哪些操作、查看哪些数据:一个列出 commands 和读取操作的 Api,并带有 middleware(sessions、MFA)。每个 block 自行执行授权:command 在 中授权,view 或 stream 在其 Layer 的
decide中授权,query 在其 handler 中授权。validate
跨越 aggregates 的规则绝不变成 cross-aggregate transaction。为不变量创建独立的 aggregate,并以它所保护的对象作为 key(例如,以 username 为 key 的 )。或者在 policy 或 saga 中先预留、后提交,并为所有等待中的操作设置 deadline。
UsernameClaim更多:什么是领域驱动设计?、领域模型。
Conventions
约定
- One block per file, named . The kinds are
<Noun>.<kind>.ts,.aggregate,.command,.view,.policy,.saga,.projection,.stream,.query,.externaland.api. Plus.model,.middlewareand.context. An aggregate's id lives in.rejectionnext to it. Plain values (<Aggregate>.id.ts) and helpers keep plain names.Money.ts - A command file holds the command, the events it records, and the rejections only it returns. A rejection or event used by several commands gets its own file.
- A block's implementation sits in its file (in
TransferExecutionLive), unless it touches infrastructure. Then it lives in the server package.TransferExecution.policy.ts - Folders are subdomains (,
Account/). The model, Apis, middleware and request context sit at the package root.Transfer/ - The domain package imports only and
@alchemy.run/domain. Cloudflare, Stripe, databases and HTTP session handling belong to the server package.effect - One domain package per model (bounded context). A second model is a second domain package; one server can host several.
More: Project layout.
- 每个 block 单独一个文件,命名为 。 kind 包括
<Noun>.<kind>.ts、.aggregate、.command、.view、.policy、.saga、.projection、.stream、.query、.external和.api,此外还有.model、.middleware和.context。aggregate 的 id 位于其旁边的.rejection中。普通值(<Aggregate>.id.ts)和 helpers 保持不带 kind 后缀的普通名称。Money.ts - command 文件包含该 command、它记录的 events,以及仅由它返回的 rejections。 被多个 commands 使用的 rejection 或 event 应单独放入一个文件。
- block 的实现位于其文件中(例如 位于
TransferExecutionLive),除非它涉及基础设施;此时应放在 server package 中。TransferExecution.policy.ts - 文件夹表示 subdomains(、
Account/)。model、Apis、middleware 和 request context 位于 package 根目录。Transfer/ - domain package 仅导入 和
@alchemy.run/domain。 Cloudflare、Stripe、数据库和 HTTP session handling 属于 server package。effect - 每个 model 使用一个 domain package(bounded context)。第二个 model 对应第二个 domain package;一个 server 可以托管多个 model。
更多:项目布局。
Rules that keep a model correct
保持模型正确的规则
- is pure, or an
decidethat reads only what its command's middleware provides (Effect.fn). It returns events or a rejection; it never performs I/O.yield* CurrentActor - only applies an event to state. It never decides anything. It and
evolveare total: they cover every state and never fail, becausereplyalready refused what can't happen. Keep a state the event can't follow (decide), never a guard that throws.s._tag === "Open" ? { ...s, balance: e.balanceAfter } : s - Events carry the outcome worked out (
decide), sobalanceAftercopies it instead of repeating a rule.evolve - Every rejection a command can return is listed in its . Callers handle each one with
rejects.Effect.catchTag - Serve a store a projection writes through a on an Api, never a hand-written route: its handler checks the caller and scopes the rows it reads.
Domain.Query - Clients never read an aggregate. Read through a view (one key, live), a stream, or a query; a query's handler may read an aggregate's latest state with then
yield* Account.accounts.get(ref) - Policies and projections run at least once. Make their effects idempotent: commands sent from a handler get deterministic ids, and stores key rows by .
stream:seq - Don't wait for a saga in a policy. it, keyed by the aggregate it's about, and react to the events its steps record.
start - A handler resolves its dependencies once in and requires nothing per call.
toLayer - Build one Layer and provide it once (,
Layer.mergeAll). Never chainLayer.provide.Effect.provide - Never . Fail with tagged errors (
throw,Domain.Rejection).Schema.TaggedError - Tracing is automatic. Record payload values only with in a handler: it lands on the engine's span for that handler. Make secrets
Effect.annotateCurrentSpan, so they never reach a trace or a log.Schema.Redacted - Test what the model does, through stories: events, replies, rejections, views and saga outcomes. Don't test a saga's internal steps.
- 必须是纯粹的,或者是一个只读取其 command 的 middleware 所提供内容的
decide(Effect.fn)。它返回 events 或 rejection,绝不执行 I/O。yield* CurrentActor - 只将 event 应用到 state,绝不进行任何决策。它与
evolve都是 total 的:完整覆盖每一种 state 且绝不失败,因为reply已经拒绝不可能发生的情况。对于 event 无法跟随的 state,保持原状(decide),绝不要使用会抛出异常的 guard。s._tag === "Open" ? { ...s, balance: e.balanceAfter } : s - Events 携带 已计算出的结果(
decide),因此balanceAfter应直接复制该结果,而不是重复规则。evolve - command 可能返回的每一种 rejection 都必须列在其 中。调用方使用
rejects处理每一种 rejection。Effect.catchTag - 为 projection 写入的 store 提供服务时,应通过 Api 上的 完成,绝不编写手写 route:其 handler 检查 caller,并限定所读取 rows 的范围。
Domain.Query - Clients 绝不读取 aggregate。应通过 view(单个 key、实时数据)、stream 或 query 读取;query 的 handler 可以通过 读取 aggregate 的最新 state,然后调用
yield* Account。accounts.get(ref) - Policies 和 projections 至少执行一次。确保其 effects 具备幂等性:handler 中发送的 commands 使用确定性 ids,stores 使用 作为 rows 的 key。
stream:seq - 不要在 policy 中等待 saga 完成。应启动 saga,以其关联的 aggregate 为 key,并通过其各步骤记录的 events 作出响应。
- Handler 应在 中一次性解析依赖,每次调用时不再需要解析任何依赖。
toLayer - 构建一个 Layer 并一次性提供它(、
Layer.mergeAll)。绝不串联Layer.provide。Effect.provide - 绝不 。应使用带 tag 的 errors 失败(
throw、Domain.Rejection)。Schema.TaggedError - Tracing 自动完成。仅在 handler 中使用 记录 payload 值:它会落到该 handler 对应的 engine span 上。将 secrets 设为
Effect.annotateCurrentSpan,确保它们绝不会进入 trace 或 log。Schema.Redacted - 通过 stories 测试模型的行为,包括 events、replies、rejections、views 和 saga outcomes。不要测试 saga 的内部步骤。
Checklist for a change
变更检查清单
- Name the new facts, commands and rejections. Put each command in its own with its events and rejections.
<Command>.command.ts - Add the command to its aggregate's , with a
commandsbranch for it and andecidebranch for each event.evolve - Update the views, policies and projections that care about the new events.
- Expose it through an Api, if a client sends or reads it, with the middleware it needs, and give it a as its last argument.
{ description } - Write stories for the happy path, every rejection, every policy reaction, and every query's authorization.
- Run the domain package's tests, with one that expects to be empty. Then type-check the monorepo (
Domain.verify(Model)) and run the server and web tests.tsc -b
- 为新的 facts、commands 和 rejections 命名。将每个 command 放入各自的 ,并包含其 events 和 rejections。
<Command>.command.ts - 将 command 添加到对应 aggregate 的 中,为它添加
commands分支,并为每个 event 添加decide分支。evolve - 更新关注新 events 的 views、policies 和 projections。
- 如果 client 会发送或读取该功能,请通过 Api 暴露它,并提供所需的 middleware,同时将 作为最后一个参数。
{ description } - 为 happy path、每一种 rejection、每一种 policy reaction 和每个 query 的授权编写 stories。
- 运行 domain package 的测试,其中一个测试应期望 的结果为空。然后对 monorepo 进行类型检查(
Domain.verify(Model)),并运行 server 和 web 测试。tsc -b