一个后端系统到底需要多少东西?路由、请求校验、认证、限流、后台任务、定时任务、发布订阅、缓存、缓存失效、密钥管理、重试、死信处理、可观测性、API文档、给前端用的类型化客户端、Docker配置……数下来大概四十项。

每一项通常对应一个独立的库,每个库有自己的心智模型、自己的失败模式、自己的报错方式——或者干脆不报错。真正的成本不在"40个库"本身,而在它们之间的接缝:认证中间件不知道队列消费者的上下文,定时任务不知道HTTP校验层对坏请求做了什么,你手动接的缓存失效逻辑六个月后就忘了。

打开网易新闻 查看精彩图片

作者自己数过,用约40个独立关注点拼一个朴素技术栈,会产生大约136个接缝——两套系统需要就某件事达成一致,却没有任何机制保证它们真的会一致。

okengine的赌注:这40个关注点大部分不是独立的

它们其实是八种原语穿了不同的衣服。与其为每个库建模,不如为原语建模。在okengine自己的架构里,接缝数从136掉到了48个——因为过去"两个系统靠约定达成契约"的地方,变成了"一条声明,编译器来检查"。

这个说法够具体,可以直接拿元素清单来验证。下面看两个实际例子。

端点、定时任务、队列消费者:同一个想法穿了三件戏服

大多数框架给每种场景一套不同的API:app.get()路由,某个调度库给定时任务,队列客户端带着自己的连接/确认/重试词汇表给消费者。结果是你得学三套测试策略、三种看堆栈的方式、三种输入校验方法——而底下问的是同一个问题:"给定这个输入,当这件事发生时,做这个,以及这里可能出什么错。"

okengine的答案是:只有一种形状,叫Flow,变的只是触发器。

export const createOrder = on(http.post("/orders"),flow("orders.create", {in: z.object({ sku: z.string(), qty: z.number().int().min(1) }),out: z.object({ id: z.string() }),errors: { OutOfStock: z.object({ left: z.number() }) },do: async (input, fx) => {const id = fx.id();await fx.store(db).insert(orders).values({ id, ...input, status: "pending" });return { id };},}),

http.post("/orders")换成every("1h")(定时任务),或者换成另一个Flow发出的事件,或者数据库某行发生变化——右边的flow(...)形状完全不变。in/out是运行时检查的契约(Standard Schema标准,兼容Zod、Valibot等),不只是编译期类型,所以"请求看起来像……"这类问题在运行时就被拦住了。