architecture-boundaries
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseArchitecture and layer boundaries
架构与分层边界
When to use: Layering and boundaries, web vs public API, app layout (clients, routes, logging), ports/adapters, runtime-portable domain/shared/utils code, multi-tenancy, DDD layout, or anti-patterns.
适用场景: 分层与边界、Web与公开API、应用布局(客户端、路由、日志)、端口/适配器、可跨运行时的领域/共享/工具代码、多租户、DDD布局以及反模式。
App boundaries (apps/*
)
apps/*应用边界(apps/*
)
apps/*Apps only handle:
- Input validation
- Authentication and authorization
- Organization access enforcement
- Routing to domain use-cases
No business logic in handlers, controllers, or jobs.
应用仅处理以下内容:
- 输入验证
- 身份认证与授权
- 组织访问权限管控
- 路由至领域用例
处理程序、控制器或作业中不得包含业务逻辑。
Application layout (apps/*
)
apps/*应用布局(apps/*
)
apps/*- Clients: Initialize integrations in and import from boundaries — avoid scattering raw clients.
apps/*/clients.ts - Routes: Use with a
apps/*/routes/(or equivalent) pattern so the HTTP surface stays modular.registerRoutes() - Logging: Use from
createLogger()with a stable service name per app.@repo/observability - Tracing: Every call site must include
Effect.runPromisefromwithTracingin the pipe chain to connect Effect spans to the OTel pipeline. See effect-and-errors for the full tracing rules.@repo/observability - Configuration values: Read env through /
parseEnv— see env-configuration.parseEnvOptional
- 客户端:在中初始化集成,并从边界导入——避免分散原始客户端实例。
apps/*/clients.ts - 路由:使用目录,并采用
apps/*/routes/(或等效)模式,确保HTTP接口保持模块化。registerRoutes() - 日志:使用中的
@repo/observability,为每个应用指定稳定的服务名称。createLogger() - 链路追踪:每个调用点必须在管道链中包含
Effect.runPromise的@repo/observability,以将Effect跨度连接到OTel管道。完整的追踪规则请参阅effect-and-errors。withTracing - 配置值:通过/
parseEnv读取环境变量——请参阅env-configuration。parseEnvOptional
Web vs public API (apps/web
and apps/api
)
apps/webapps/apiWeb与公开API(apps/web
和 apps/api
)
apps/webapps/api- is the stable public API surface. Treat its routes/contracts as externally consumed and evolve them carefully.
apps/api - must not call or proxy through
apps/webfor internal product features.apps/api - For web product development, implement backend behavior in server functions by composing domain use-cases and platform adapters directly.
apps/web - Keep iteration velocity in by adding web-private server functions/stores while preserving
apps/webstability.apps/api - Shared business rules still belong in domain packages; and
apps/webshould both orchestrate domain use-cases rather than duplicating policy.apps/api - Latitude product capabilities should be equally accessible to humans through the web UI and to other LLM agents through MCP/API surfaces.
- Do not dead-end product behavior into UI-only flows. Preserve the boundary rules above, but design schemas, use-cases, and public capabilities so machine-facing access can exist without redesign.
- For the concrete recipe — ,
defineApiEndpointfactories,createXxxRoutes/pnpm openapi:emit, schema-description rules that fan out to the TS SDK and MCP tools — see api-endpoints.pnpm mcp:emit
- 是稳定的公开API接口。需将其路由/契约视为外部消费内容,谨慎进行版本演进。
apps/api - 不得为内部产品功能调用或代理
apps/web。apps/api - 对于Web产品开发,应通过直接组合领域用例和平台适配器,在的服务器函数中实现后端行为。
apps/web - 在保持稳定性的同时,通过添加Web私有服务器函数/存储来提升
apps/api的迭代速度。apps/web - 共享业务规则仍属于领域包;和
apps/web都应编排领域用例,而非重复实现策略。apps/api - Latitude产品功能应同时支持通过Web UI供人类访问,以及通过MCP/API接口供其他LLM Agent访问。
- 不得将产品行为局限于仅UI可用的流程。遵守上述边界规则,同时设计模式、用例和公开功能,以便无需重新设计即可支持机器端访问。
- 具体实现方案——、
defineApiEndpoint工厂、createXxxRoutes/pnpm openapi:emit、可生成TS SDK和MCP工具的模式描述规则——请参阅api-endpoints。pnpm mcp:emit
Cross-cutting implementation constraints
跨领域实现约束
- Public request/response schemas should remain boundary-specific; they may reuse shared domain schemas or narrower projections rather than forcing full domain entities onto every surface.
- When a capability is part of the product contract, preserve a machine-facing MCP/API surface instead of making it web-only.
- 公开请求/响应模式应保持边界特定;可复用共享领域模式或更窄的投影,而非强制所有接口使用完整领域实体。
- 当某项功能属于产品契约的一部分时,需保留面向机器的MCP/API接口,而非仅提供Web端访问。
Domain layer (packages/domain/*
)
packages/domain/*领域层(packages/domain/*
)
packages/domain/*Business logic lives here. Domain packages expose:
- Use-cases
- Canonical entity schemas and inferred entity types
- Domain types and errors
- Dependency ports (interfaces/tags)
业务逻辑存放在此处。领域包对外暴露:
- 用例
- 标准实体模式及推导的实体类型
- 领域类型与错误
- 依赖端口(接口/标签)
Domain package layout
领域包布局
Domain entities are Zod-first: + in . See and .
entitySchemaz.infer<typeof entitySchema>src/entities/<entity>.tsdev-docs/domain-entities.mddocs/adr/0001-domain-entity-schema-style.md- Treat canonical domain entity schemas as the source of truth. Schemas and types elsewhere in the same domain, plus app/platform boundary schemas, should derive from or reuse the entity shapes whenever practical instead of re-declaring the same fields.
- When a boundary schema must differ materially from the entity shape, still reuse the relevant domain constants, field schemas, and literal unions rather than hardcoding duplicated lengths or sentinel values again.
- Canonical entity schemas and their inferred entity types belong in .
packages/domain/*/src/entities/<entity>.ts - Domain package constants belong in .
packages/domain/*/src/constants.ts - Domain package errors belong in . A full package-by-package inventory and import rules live in
packages/domain/*/src/errors.ts.dev-docs/domain-errors.md - For how to structure those errors (tagged classes, HTTP fields, unions per flow, naming), treat as the reference: see
packages/domain/issuesand the section Domain errors (packages/domain/issues/src/errors.tsreference pattern) in@domain/issues.dev-docs/issues.md - Small domain-scoped shared helpers such as predicates or lifecycle helpers belong in .
packages/domain/*/src/helpers.ts - Types and schemas that exist only as inputs to one domain use-case belong in that use-case file rather than a generic side module, unless several use-cases truly share the exact same contract.
- App and platform layers should build boundary-specific schemas by reusing or deriving from domain entity/use-case schemas whenever practical rather than redefining the same contract from scratch.
领域实体采用Zod优先原则:在中定义 + 。请参阅和。
src/entities/<entity>.tsentitySchemaz.infer<typeof entitySchema>dev-docs/domain-entities.mddocs/adr/0001-domain-entity-schema-style.md- 将标准领域实体模式视为事实来源。同一领域内其他位置的模式和类型,以及应用/平台边界模式,应尽可能从实体结构派生或复用,而非重新声明相同字段。
- 当边界模式必须与实体结构存在实质性差异时,仍需复用相关领域常量、字段模式和字面量联合类型,而非再次硬编码重复的长度或标记值。
- 标准实体模式及其推导的实体类型应放在中。
packages/domain/*/src/entities/<entity>.ts - 领域包常量放在中。
packages/domain/*/src/constants.ts - 领域包错误放在中。完整的包级错误清单及导入规则请参阅
packages/domain/*/src/errors.ts。dev-docs/domain-errors.md - 关于如何构建这些错误(标记类、HTTP字段、按流程划分的联合类型、命名规则),请以为参考:查看
packages/domain/issues以及packages/domain/issues/src/errors.ts中的*领域错误(dev-docs/issues.md参考模式)*章节。@domain/issues - 小型领域范围共享助手(如谓词或生命周期助手)放在中。
packages/domain/*/src/helpers.ts - 仅作为单个领域用例输入的类型和模式应放在该用例文件中,而非通用侧边模块,除非多个用例确实共享完全相同的契约。
- 应用层和平台层应尽可能通过复用或派生领域实体/用例模式来构建边界特定模式,而非从头重新定义相同契约。
Infrastructure (packages/platform/*
)
packages/platform/*基础设施层(packages/platform/*
)
packages/platform/*Infrastructure details live here only. Platform packages implement adapters for domain ports.
仅存放基础设施相关细节。平台包实现领域端口的适配器。
Platform adapters: Effect-based clients
平台适配器:基于Effect的客户端
Reference implementation: — (and the thin wrapper used by scripts).
packages/platform/db-weaviate/src/client.tscreateWeaviateClientEffectcreateWeaviateClientUse this pattern when a platform package owns an external SDK client so composition roots can stay in Effect and errors stay typed.
- Primary constructor is an Effect — Export (or with requirements
createXClientEffect(...): Effect.Effect<Client, E, never>if unavoidable). Scripts and one-off CLIs may exportRasasync function createXClient()only at the boundary that needs promises.Effect.runPromise(createXClientEffect(...)) - Typed errors — Model connection, validation, and bootstrap failures with (or shared env errors from
Data.TaggedError). Union them into a single@platform/env(or similar) exported next to the constructor.CreateXClientError - Configuration — Resolve settings with /
parseEnvfromparseEnvOptionalinside the Effect pipeline, not ad hoc@platform/envreads scattered outside the client module.process.env - Interop — Wrap promise-based SDK calls in and map failures to tagged errors. Compose steps with
Effect.tryPromise,Effect.pipe, andEffect.flatMap.Effect.map - Bootstrap in the pipeline — If the client must apply schema/migrations/health checks before use, run those as Effects in the same pipeline (see Weaviate: after connect) so callers get a ready client or a single error channel.
migrateWeaviateCollectionsEffect - Live layers — Expose a thin layer for the external SDK client and keep repository adapters as
XClientLive(client, scope...)orLayer.effectvalues that depend on that client service as needed. The composition root acquires the client withLayer.succeedand provides it via a small helper when useful, for examplecreateXClientEffect.withWeaviate(IssueProjectionRepositoryLive, client, organizationId)
Not every legacy adapter has been migrated; prefer this shape for new work and when touching client construction.
参考实现: —— (以及脚本使用的轻量包装器)。
packages/platform/db-weaviate/src/client.tscreateWeaviateClientEffectcreateWeaviateClient当平台包管理外部SDK客户端时,使用此模式,以便组合根保持在Effect中,且错误保持类型化。
- 主构造函数为Effect —— 导出(若不可避免则包含依赖
createXClientEffect(...): Effect.Effect<Client, E, never>)。脚本和一次性CLI仅可在需要Promise的边界处导出R,实现为async function createXClient()。Effect.runPromise(createXClientEffect(...)) - 类型化错误 —— 使用(或
Data.TaggedError中的共享环境错误)建模连接、验证和引导失败。将它们合并为单个@platform/env(或类似名称)并与构造函数一起导出。CreateXClientError - 配置 —— 在Effect管道内使用的
@platform/env/parseEnv解析设置,而非在客户端模块外分散读取parseEnvOptional。process.env - 互操作性 —— 将基于Promise的SDK调用包装在中,并将失败映射为标记错误。使用
Effect.tryPromise、Effect.pipe和Effect.flatMap组合步骤。Effect.map - 管道内引导 —— 如果客户端在使用前必须应用模式/迁移/健康检查,则在同一管道中作为Effect运行这些操作(参考Weaviate:连接后执行),以便调用者获得就绪的客户端或单一错误通道。
migrateWeaviateCollectionsEffect - Live层 —— 为外部SDK客户端暴露轻量的层,并将存储库适配器保留为依赖该客户端服务的
XClientLive(client, scope...)或Layer.effect值。组合根通过Layer.succeed获取客户端,并在需要时通过小助手提供,例如createXClientEffect。withWeaviate(IssueProjectionRepositoryLive, client, organizationId)
并非所有遗留适配器都已迁移;新开发和修改客户端构造时优先采用此结构。
Shared utilities (packages/utils
)
packages/utils共享工具(packages/utils
)
packages/utilsGeneral-purpose utility functions that can be shared across any package (domain, platform, or app) live in . This package should contain pure, stateless helper functions with no domain or infrastructure dependencies.
@repo/utilsExamples: , , string helpers, number formatters.
formatCountformatPriceWhen writing a utility function that is not specific to a single domain or package, place it in instead of keeping it local.
@repo/utils可在任何包(领域、平台或应用)中共享的通用工具函数存放在中。此包应包含纯无状态助手函数,无领域或基础设施依赖。
@repo/utils示例:、、字符串助手、数字格式化工具。
formatCountformatPrice当编写不特定于单个领域或包的工具函数时,应将其放在中,而非本地保留。
@repo/utilsShared domain vs utils
共享领域与工具包的区别
@domain/shared@repo/utils- Use for domain-level shared contracts, types, errors, and IDs used across bounded contexts.
@domain/shared - Use for global pure, stateless helpers that are reusable anywhere.
@repo/utils - If a helper has domain/business meaning, it belongs in ; otherwise, use
@domain/shared.@repo/utils
@domain/shared@repo/utils- 使用存放跨限界上下文使用的领域级共享契约、类型、错误和ID。
@domain/shared - 使用存放可在任何地方复用的全局纯无状态助手。
@repo/utils - 如果助手具有领域/业务含义,则属于;否则使用
@domain/shared。@repo/utils
Ports and adapters
端口与适配器
- Domain depends on interfaces/tags only (ports like ,
Repository,CacheStore)Publisher - Platform packages implement adapters
- Composition roots in apps provide live layers
- Domain must never import concrete DB/cache/queue/object storage clients
- Repository method names: Use the standard verbs in dev-docs/repositories.md (,
findByIdfor unique keys,findByXxx/listByXxxfor collections,list,savevsdelete, etc.).softDelete - Reliability async contracts should stay project-scoped as well as organization-scoped: include both and
organizationIdin event/task/workflow payloads by default (exceptprojectId,MagicLinkEmailRequested,UserDeletionRequested,domain-events, andmagic-link-emailpayloads).user-deletion
- 领域仅依赖接口/标签(如、
Repository、CacheStore等端口)Publisher - 平台包实现适配器
- 应用中的组合根提供Live层
- 领域绝不能导入具体的数据库/缓存/队列/对象存储客户端
- 存储库方法命名:使用dev-docs/repositories.md中的标准动词(、针对唯一键的
findById、针对集合的findByXxx/listByXxx、list、savevsdelete等)。softDelete - 可靠性异步契约应同时支持项目范围和组织范围:默认在事件/任务/工作流负载中包含和
organizationId(projectId、MagicLinkEmailRequested、UserDeletionRequested、domain-events和magic-link-email负载除外)。user-deletion
Web standards first (domain, utils, shared)
Web标准优先(领域、工具、共享包)
In , , , or any code that may run outside Node (browser, edge, isolates), prefer Web Standard APIs over Node-only modules so those layers stay portable.
packages/domain/*packages/utils@domain/shared- Use /
crypto.subtleinstead ofcrypto.getRandomValuesnode:crypto - Use instead of Node-specific HTTP clients
fetch - Use /
TextEncoderinstead ofTextDecoderBuffer.from(…, 'utf-8') - Use for binary data in public interfaces
Uint8Array - Use instead of
ReadableStream/node:streamstreamsnode:fs - Use ,
URL,URLSearchParams,Headers,Requestfrom the global scopeResponse - Use instead of JSON round-trips for deep cloning
structuredClone
Node-only APIs are acceptable in build tooling, scripts, CLI utilities, and test infrastructure. If you need Node outside those scopes, add a brief comment explaining why.
在、、或任何可能在Node外运行的代码(浏览器、边缘计算、隔离环境)中,优先使用Web标准API而非Node专属模块,以保持这些层的可移植性。
packages/domain/*packages/utils@domain/shared- 使用/
crypto.subtle替代crypto.getRandomValuesnode:crypto - 使用替代Node专属HTTP客户端
fetch - 使用/
TextEncoder替代TextDecoderBuffer.from(…, 'utf-8') - 在公开接口中使用处理二进制数据
Uint8Array - 使用替代
ReadableStream/node:stream流node:fs - 使用全局作用域的、
URL、URLSearchParams、Headers、RequestResponse - 使用替代JSON往返进行深度克隆
structuredClone
Node专属API可用于构建工具、脚本、CLI工具和测试基础设施。如果在这些范围外需要使用Node API,请添加简短注释说明原因。
Data and infrastructure (overview)
数据与基础设施概述
- Postgres: Control-plane and relational data (users, organizations, memberships, config)
- ClickHouse: High-volume telemetry storage and analytical reads
- Weaviate: Vector database for embeddings storage and semantic similarity search
- Redis: Cache and BullMQ backend
- Object storage: Durable raw ingest payload buffering
For access patterns, schema, and migrations, see database-postgres and database-clickhouse-weaviate.
- Postgres:控制平面和关系型数据(用户、组织、成员身份、配置)
- ClickHouse:高容量遥测存储和分析读取
- Weaviate:用于嵌入存储和语义相似度搜索的向量数据库
- Redis:缓存和BullMQ后端
- 对象存储:持久化原始摄入负载缓冲
访问模式、模式和迁移相关内容,请参阅database-postgres和database-clickhouse-weaviate。
Multi-tenancy
多租户
- Every request is organization-scoped
- A user may belong to many organizations
- Organization membership checks happen at boundaries before domain execution
- All telemetry persistence and query paths include
organizationId - Organization-scoped Redis or cache keys must start with ; keep the org id first in the key
org:${organizationId}:...
- 每个请求都属于特定组织范围
- 一个用户可属于多个组织
- 组织成员身份检查在边界处执行,之后再进行领域逻辑处理
- 所有遥测持久化和查询路径均包含
organizationId - 组织范围的Redis或缓存键必须以开头;将组织ID放在键的首位
org:${organizationId}:...
Domain design (DDD)
领域设计(DDD)
- Organize by bounded context (e.g. telemetry, organizations, identity, alerts)
- Domains should be single-responsibility and focused on policy/rules
- Use in-memory adapters for fast tests where possible
- 按限界上下文组织(如遥测、组织、身份、告警)
- 领域应单一职责,专注于策略/规则
- 尽可能使用内存适配器进行快速测试
Anti-patterns to reject
需避免的反模式
- Cross-domain logic without clear ownership
- New provider integrations without a core capability contract
- Introducing application env vars without the prefix (see env-configuration)
LAT_ - Using or
"use client"directives — these are Next.js-specific; the web app uses TanStack Start"use server" - Exporting test utilities from a package's main entry point (see testing)
- 无明确所有权的跨领域逻辑
- 无核心功能契约的新供应商集成
- 未使用前缀的应用环境变量(请参阅env-configuration)
LAT_ - 使用或
"use client"指令——这些是Next.js专属指令;Web应用使用TanStack Start"use server" - 从包的主入口导出测试工具(请参阅testing)