Backend 架构 Pattern

读者:新成员、跨 repo 切换的工程师、需要解释 retaintive backend 如何工作的 PM / founder / AI Agent。

本文讲 backend 运行机制和 repo 内代码分层。系统级 repo 地图见 系统架构总览,通话/SMS/Lead 数据管道见 Backend 数据管道


TL;DR

retaintive 有两类 backend,不应该混成一个东西:

Backend面向谁触发方式架构 pattern主要职责
studio-website-monorepo/apps/apiapps/web / 浏览器HTTP requestLayered:routes → services → repositories;部分 v3 read route 仍 inline SQL鉴权、store scope、查询/少量写入、返回 JSON
callytics-infrastructure/lambda/*SQS / EventBridge / 内部 pipelineasync eventHexagonal where complexity warrants it:handler → core ports → infrastructure adapters转录、AI 分析、短信持久化、联系人分析、Lead 下游处理、报告
callytics-common/src/db多 repocompile-time importshared schemaDrizzle schema + TypeScript types 单源

两个 backend 都遵守同一个方向:业务规则不应该依赖具体 vendor / framework / database client。差别是抽象深度不同。


1. 基础词汇

Web vs API

apps/web 是前端应用。它被 build 成 HTML/JS/CSS,部署在 Cloudflare Pages。用户浏览器下载这些静态文件后,JavaScript 在浏览器里运行。

apps/api 是 backend API。它被 build 成 Lambda bundle,部署在 AWS Lambda,通过 API Gateway 暴露成 studio-api*.retaintive.ai

前端调用后端的本质是 HTTP:

Browser JavaScript
  → Hono RPC fetch client(preferred)或 legacy axios client
  → https://studio-api*.retaintive.ai/v3/...
  → API Gateway
  → Lambda running Hono app
  → JSON response

API backend vs processing backend

apps/api前端查询 API。它不负责从 RingCentral 下载录音,也不负责调用 Deepgram / OpenRouter 做 AI 分析。

这些后台处理在 callytics-infrastructure/lambda/*

RingCentral Webhook / SQS / EventBridge
  → Lambda processors
  → Neon / DynamoDB / S3
  → apps/api later reads the result

Route / Service / Repository

在 server-side API 里,不建议用 MVC 描述 backend。更准确的是:

职责retaintive 例子
Route / Controller解析 HTTP input、调用 auth/context、返回 JSONroutes/v3/contacts.ts
Service / Use case业务规则、权限判断、transaction boundary、跨 repository 协调getStoreContext() 当前承担一部分 service 职责
Repository / SQL和数据库对话raw parameterized SQL、infrastructure/neon-repository.ts

2. studio-api request lifecycle

一次用户访问 Dashboard 的 API 请求,大致是这个链路:

这条链路解释了几个常见疑问:

  • 为什么前端能传数据给后端? 因为浏览器发 HTTP request,不是因为 frontend/backend 在同一个 repo。
  • 为什么后端能读数据库? 因为 API Lambda 部署时配置了环境变量、secret、network、IAM 和 database connection。
  • auth 在哪里发生? Cognito 发 token;前端 API client 附上 access token;API middleware 验证 token;route/helper 再检查 store-level 权限。
  • API 和业务处理 Lambda 的关系是什么? API 通常读 processing Lambda 写好的结果;processing Lambda 不由浏览器直接调用。

本地代码里当前同时存在两套前端 API client:

Client文件状态
Hono RPC fetch clientapps/web/src/api/hono-client.tstyped-api.ts新代码 preferred,端到端复用 AppType 类型,内置 401 refresh/retry
legacy axios clientapps/web/src/api/index.tsot-api/base.tsot-api/*.ts 仍在用,后续迁移掉

3. studio-api 的 Layered 架构

studio-website-monorepo/apps/api 选择 layered,是因为它主要是 read-heavy API:复杂查询、筛选、排序、分页、少量写入。目标结构是 routes/ → services/ → repositories/,但迁移仍在进行:简单 v3 Neon read route 仍会直接在 route 里写 parameterized SQL。

3.1 Route 层负责什么

Route 是 HTTP 边界,应该处理:

  • 读取 c.req.query() / c.req.json()
  • 用 Zod 做 input validation
  • 从 middleware 读取 auth
  • 调用 service/helper
  • 把结果转成 JSON response
  • 把错误交给统一 error handler

Route 不应该承载复杂业务规则。读查询很薄时可以 inline SQL;一旦规则开始变多,就应该抽 service。

3.2 Services 和 Auth / Store context

代码里已经有明确的 src/services/src/repositories/

  • src/services/*-neon.ts:Neon-backed business/data helpers,例如 stores、members、phone numbers、OAuth states。
  • src/services/ringcentral/*token-access.tssubscription-client.ts:外部服务和 cross-service 调用。
  • src/repositories/*:仍保留的 DynamoDB data access,例如 call-analysis.tsphone-store-assignments.ts
  • src/utils/authorization.ts / store-authorization-neon.ts:store-level authorization helper。

getStoreContext(userId, storeId) 本质上是 service 层逻辑,因为它做了业务判断:

1. store 是否存在
2. 当前 user 是 owner 还是 member
3. user 是否有权限访问这个 store
4. 这个 store 下有哪些 phone numbers

它现在放在 routes/v3/shared.ts 是务实选择:多个 v3 route 共用,且这些 endpoint 是 read-heavy Neon SQL。语义上仍要把它当 service boundary 看,而不是“普通 util”。其他 v2/v3 route 也可能用 getAuthorizedStore()getAuthorizedStoreNeon() 做同类 authorization。

3.3 SQL / Repository 层负责什么

studio-api 里大量查询是 analytical read:CTE、多 JOIN、动态 filters、pagination、sort。用 raw SQL 更直接,也更容易看执行计划。

纪律是:

  • 必须用 parameterized query:$1, $2, $N
  • 用户输入只能进入 params array,不能拼进 SQL string
  • store_id / tenant scope 必须在 SQL 条件里明确出现
  • 同一查询逻辑复制到 3 个以上 endpoint 时,抽 repository/helper

3.4 什么时候必须抽 service

下面情况不要继续把逻辑塞进 route:

信号为什么
出现业务规则规则要可测试、可复用,不能藏在 HTTP handler
多表写入 / transactiontransaction boundary 需要集中控制
调外部 vendorvendor client 应该是 adapter,不应散落在 route
同一段 SQL/逻辑 3+ 处复用会产生行为漂移
route 超过“parse input + call helper + return JSON”可读性和测试性开始下降

目标不是教条地建立 services/repositories/ 目录,而是让 boundary 清楚。


4. Authentication 和 store-level authorization

认证和授权是两层:

回答的问题
Authentication“这个请求是谁发的?”
Authorization“这个 user 能不能访问这个 store / org / resource?”
Data scoping“SQL/DDB 查询有没有被限制在允许的数据范围内?”

不要把“有 token”理解成“能访问所有数据”。token 只证明身份;store-level authorization 决定数据范围。

本地 test 环境还有一个专用旁路:X-Test-User-Id header 可以跳过 JWT,用于 integration/dev test;prod 不允许。


5. callytics-infrastructure 的 Hexagonal 架构

callytics-infrastructure/lambda/* 的复杂 processor 选择 hexagonal,是因为后台处理业务复杂、外部依赖多、需要单元测试时 mock vendor/AWS。transcribe-processorai-analysis-processor 是最明确的 reference;message-processor 也拆成 core/infrastructure/lead-processor 这类较薄的 SQS consumer 则更接近 thin handler + core function。

  • Deepgram / AWS Transcribe
  • DeepSeek V4 Flash via OpenRouter
  • SQS
  • DynamoDB
  • S3
  • Neon
  • Secrets Manager

核心思想:core 定义自己需要什么能力(ports),infrastructure 提供具体实现(adapters)

典型目录:

lambda/ai-analysis-processor/src/
├── handler.ts                         # driving adapter + composition root
├── core/
│   ├── models.ts                      # domain types
│   ├── protocols.ts                   # ports
│   └── stages/                        # pipeline / use cases
└── infrastructure/
    ├── ai/                            # model provider adapter
    ├── config-repository.ts           # S3/config adapter
    ├── neon-repository.ts             # Postgres adapter
    ├── persistence-repository.ts      # DDB/S3 adapter
    ├── secrets-manager.ts             # Secrets Manager adapter
    └── sqs-publisher.ts               # SQS adapter

5.1 handler.ts 是 composition root

handler.ts 允许做这些事:

  • import AWS SDK
  • 初始化 AWS clients
  • 读取 env vars
  • 创建 infrastructure adapters
  • 把 adapters 注入 core pipeline
  • 处理 SQS batch response / retry / partial failure

core/ 不应该 import AWS SDK,也不应该知道 Lambda/SQS/EventBridge 的存在。

5.2 Core 只依赖 ports

Core 应该写成:

async function runPipeline(input: PipelineInput, deps: {
  configRepo: ConfigRepository;
  aiClient: AiClient;
  analysisRepo: AnalysisRepository;
  taskPublisher: TaskPublisher;
}) {
  const config = await deps.configRepo.load(input.clientId);
  const result = await deps.aiClient.analyze(input.transcript, config);
  await deps.analysisRepo.save(result);
  await deps.taskPublisher.publish(result.followUpTasks);
}

这样测试时可以传 fake deps,不需要真的调用 AWS / AI vendor / Neon:

await runPipeline(input, {
  configRepo: fakeConfigRepo,
  aiClient: fakeAiClient,
  analysisRepo: inMemoryAnalysisRepo,
  taskPublisher: fakeTaskPublisher,
});

5.3 Adapters 负责技术细节

Adapter 可以知道具体 vendor:

  • NeonAnalysisRepository 知道 SQL/Drizzle
  • S3ConfigRepository 知道 bucket/key
  • OpenRouterAiClient / invokeAI wrapper 知道 API endpoint/model name
  • SqsTaskPublisher 知道 queue URL/message shape

但这些细节不能倒灌进 core。


6. Layered vs Hexagonal 怎么选

场景理由
前端读数据、筛选、分页、返回 JSONLayered抽象成本低,读查询直观
复杂 SELECT / report / CTELayered + raw parameterized SQLSQL 表达力和性能更可控
多步骤业务处理、状态机、重试、幂等Hexagonalcore 规则需要独立测试
调 OpenAI / Deepgram / Stripe / Vapi 等 vendorHexagonalvendor 必须通过 port/adapter 隔离
SQS/EventBridge Lambda processorHexagonalhandler 适合作 composition root
多表写入 transactionLayered + 显式 servicetransaction boundary 要集中
小型 CRUD endpointLayered不要为了形式上高级增加目录和 class

判断标准:不是“哪个 pattern 更高级”,而是“业务规则是否值得从 runtime/vendor 细节里剥离出来”。


7. ORM 策略:Drizzle vs raw SQL

retaintive 混用 Drizzle 和 raw SQL 是有意选择。

用法在哪为什么
Drizzle schemacallytics-common/src/db/schema/*.tsschema 单源,生成 TS types
Drizzle insert/updateprocessing Lambda repositorieswrite path 要 type-safe,字段漂移成本高
Raw parameterized SQLapps/api/src/routes/v3/*.ts复杂 read query、CTE、JOIN、window function 更清楚

安全边界:

  • raw SQL 必须 parameterized
  • table/column 名不要从用户输入动态拼接
  • sort field 必须 whitelist
  • tenant/store scope 必须显式进 where clause
  • write path 优先复用 Drizzle schema/type

8. 不要混淆的三组边界

部署边界

apps/web  → Cloudflare Pages
apps/api  → AWS Lambda + API Gateway
lambda/*  → AWS Lambda processors

同一个 repo 里的目录可以部署到不同平台;不同 repo 的 Lambda 也可能通过 SQS 在同一个 runtime pipeline 里协作。

请求边界

Browser request → apps/api
SQS/EventBridge event → callytics-infrastructure/lambda/*
RingCentral webhook → ringcentralSubscriptionService
IMAP polling → lead-tracking

看到“后端”二字时先问:这是 HTTP API backend,还是 async processing backend?

数据边界

Neon: business primary SoT for contacts/messages/leads/tasks/calls
DynamoDB: call-analysis + config/legacy
S3: files/config artifacts
callytics-common: schema/types, not storage

9. 新增功能时怎么落位

新增前端页面需要读数据

  1. apps/web 加 page/component/query hook。
  2. apps/api 加 route。
  3. Route 做 Zod validation + auth context。
  4. 需要 store 权限时调用 getStoreContext() 或同类 helper。
  5. SQL 必须带 store/org scope。
  6. 复杂逻辑超过 route 职责时抽 service/helper。

新增后台处理步骤

  1. 先确认触发源:SQS、EventBridge、manual invoke、还是 webhook。
  2. lambda/<processor>/src/core 写 use case / pipeline step。
  3. core/protocols.ts 定义需要的 ports。
  4. infrastructure/ 实现 vendor/AWS/DB adapters。
  5. handler.ts 装配 adapters。
  6. 用 fake adapters 测 core;用 integration test 覆盖 adapter 关键路径。
  7. CDK 里声明 queue/event/env/permissions。

新增共享字段或表

  1. 先改 callytics-common schema/type。
  2. 明确 writer:哪个 Lambda/API 写这个字段。
  3. 明确 reader:哪个 API/processor 读这个字段。
  4. 更新写入矩阵或对应 system-design 文档。
  5. 跨 repo 发布顺序要避免 “reader 先读不存在字段” 或 “writer 写了旧 schema”。

10. 和业界 vocabulary 对照

retaintive 说法Spring / Java.NET解释
Hono route handler@RestControllerControllerHTTP 入口
getStoreContext()@ServiceApplication service鉴权/上下文/业务规则
raw SQL / repository file@RepositoryRepositoryDB 访问
core/Domain layerDomain layer业务规则
infrastructure/Infrastructure layerInfrastructure layer外部技术实现
handler.tsmain() / DI configProgram.csComposition root
protocols.tsPorts / interfacesInterfacescore 需要的能力契约

面试或外部沟通可以这样说:

The frontend-facing API uses a pragmatic layered architecture: Hono routes validate input, shared service helpers enforce auth and store scope, then routes call parameterized SQL or repositories. The async processing Lambdas use hexagonal architecture: handler.ts is the composition root, core/ contains business rules and ports, and infrastructure/ implements adapters for AWS, Neon, S3, SQS, and AI providers.


11. 相关文档