Infrastructure——技术外部世界
Infrastructure 的名声并不算好。
一提到这个词,很多人就会想到 Port、Adapter、Repository Interface、Factory,以及一层又一层的 Integration Architecture。图里充满箭头,矩形框比箭头还多。
本系列的导言 把 Infrastructure 描述为数据从技术外部世界走向用户时经过的第一站。但这一站真正要做的事情,其实远没有这个名字听起来那么宏大。
对这篇文章来说,先采用一个简单视角就够了:
Infrastructure 是应用与那些无法被自己完全控制的东西发生接触的地方。
Backend。REST 或 GraphQL API。WebSocket 或 SSE。Local Storage。IndexedDB。Browser API。Authentication Provider。外部 SDK。另一个 Bounded Context。
这里的“无法完全控制”不一定意味着“属于另一家公司”。即使是同一家公司的 Backend 团队,也可以修改一个 Contract,而 Frontend 无法阻止。真正关键的不是组织边界,而是谁拥有这个结构、谁决定它如何继续演进。
具体技术并不是重点。Angular HttpClient、fetch、Axios 或其他 Client,都不会改变这一层的架构职责。
外部世界 ↓Infrastructure ↓自己的业务模型Infrastructure 接收外部技术结构,并以受控方式把它们翻译到自己的世界。
一开始,它不需要做更多事情。

而这正是重点:Infrastructure 完全可以很无聊。
外部世界不属于我们
Section titled “外部世界不属于我们”Backend 的 Contract 首先属于 Backend。外部 SDK 属于它的供应商。Browser API 的 Contract 属于 Browser。
这些模型不应该仅仅因为“它们最先到达前端”,就自动成为我们的 Domain Model。
一个很小的例子就足够说明问题。Backend 返回一个客户:
type CustomerDto = { id: string; first_name: string; last_name: string;};内部则使用:
type Customer = { id: string; firstName: string; lastName: string;};这个转换一点都不复杂。恰恰因为它如此普通,这个例子才有价值——现实中的很多边界就是这么不起眼。
CustomerDto 描述 Backend 如何传递一个 Customer。Customer 描述我们的应用如何理解和使用一个 Customer。只要中间有明确转换,这两个模型就可以独立演进。
这不就是一个 Mapper 吗?
Section titled “这不就是一个 Mapper 吗?”这里很容易出现一个反问:
function toCustomer(dto: CustomerDto): Customer { return { id: dto.id, firstName: dto.first_name, lastName: dto.last_name, };}如果两个结构几乎一样,为什么还要多写这些代码?
今天,这个 Mapper 的确看起来几乎多余。明天 Backend 也许会修改字段名:
first_name → givenNamelast_name → familyName理想情况下,只需要修改这里的转换。Store、Business Rule、Facade 和 Component 都不应该感知到这次变化。

一个只有五行的 Mapper 并不是 Overengineering——前提是它阻止一个外部 Contract 在五十个文件里悄悄变成事实上的 Domain Model。
如果 DTO 不受控制地穿过这条边界,继续进入 State、Application、Component 和 Template,会发生什么,DTO Leakage 一文已经详细讨论。
这篇文章只需要记住一点:一条很小的转换边界,就足以防止外部系统的结构逐渐变成我们自己的应用结构。
TypeScript 并不会保护这条边界
Section titled “TypeScript 并不会保护这条边界”前端开发里还有一个很容易被忽视的问题。
HTTP Response 来自外部来源。JSON Parsing 之后,我们拿到的只是 JavaScript Value:String、Number、Object、Array、null。
像下面这样的 Type Assertion:
const customer = response as CustomerDto;或者带泛型的 HTTP 调用:
http.get<CustomerDto>('/api/customers/42');描述的只是 Frontend 期待得到什么结构。它并不能证明 Backend 运行时真的返回了这个结构。
TypeScript 类型不会校验外部数据。 它只在 Compile Time 描述我们希望按什么结构工作。运行时数据是否符合这个结构,如果有必要,就必须在系统边界单独验证。
这项验证属于 Infrastructure。
校验、标准化、转换
Section titled “校验、标准化、转换”这条边界的工作可以归纳为几件事:
unknown / 外部数据 ↓ Infrastructure │ ├── 校验 ├── 标准化 └── 转换 ↓ 自己的模型典型职责包括:
- Runtime Validation
- 校验必填字段
- 标准化外部格式
- 转换日期值
- 受控处理
null和undefined - 转换技术 Status Code
- 把 DTO 映射为自己的模型
- 把外部系统错误转换成自己的技术错误
Runtime Validation 可以使用 Zod 之类的 Library。具体选择哪一个 Library,对架构决策并不重要。重要的是:验证发生在哪个边界。
标准化也应该保持简单和明确。
已知格式的日期可以转成 Date。可选字段可以按约定技术规则统一。外部 Status Code 可以映射成自己的技术或业务表示。
一旦某个差异已经无法通过明确的技术规则解释,Infrastructure 就不应该擅自发明业务含义。
无法解释的数据,首先应该被视为技术错误。
应用最终如何把这个错误呈现给用户,已经属于另一项职责。
不只是 DTO
Section titled “不只是 DTO”Customer 示例使用 HTTP,只是因为最容易说明问题。同样的边界存在于所有外部数据进入应用的地方。
localStorage 里的值首先只是 String 或 null。它不会因为“昨天存进去的是一个对象”,今天就自动变回那个对象。
支付供应商的 SDK 会抛出自己的 Error Class 和 Code。
WebSocket 会收到 Message,而新 Server Version 可能改变它们的结构。
这些场景都在问同一个问题:我们是直接把 Raw Value 放进应用,还是先把它转换成 Feature 真正理解的形式?
Error 本身也可能需要转换:
try { await paymentSdk.charge(order);} catch (error) { throw toPaymentInfrastructureError(error);}toPaymentInfrastructureError 可以了解这个 SDK 的 Error Code 和技术特性。应用其他部分则只需要处理自己定义、自己控制的一组技术错误。
即使以后更换支付供应商,这门“外语”也仍然停留在 Infrastructure Boundary。
防御性转换不是业务决策
Section titled “防御性转换不是业务决策”这是这篇文章最重要的一条边界。
Infrastructure 可以消除外部技术不确定性,但不应该据此实现 Business Rule。
例如外部 Contract 返回:
"ACTIVE" ↓CustomerStatus.Active这个转换可以属于 Infrastructure。
但一个 Active Customer 是否允许下单,是另一个问题:
CustomerStatus.Active ↓这个客户可以下单吗?这里才开始进入业务逻辑。
两者在代码里可能看起来极其相似。一个 switch、一个 if,甚至只是一行赋值。但它们承担的职责完全不同。
Infrastructure 可以转换:
"2026-08-10T10:15:00Z" → Datenull → undefined"C" → CreditHoldfirst_name → firstName而 +State 回答的问题是:
这个订单可以取消吗?这个客户可以下单吗?这个 State Transition 合法吗?当前业务状态下允许哪些操作?Infrastructure 消除技术和结构层面的不确定性。业务决策从它之后开始。
Infrastructure 可以做什么,不该做什么
Section titled “Infrastructure 可以做什么,不该做什么”Infrastructure 可以并且应该:
- 封装外部 Contract
- 把 DTO 转换为自己的模型
- 标准化技术格式
- 校验外部输入
- 封装技术 API
- 把外部 Error 转换成自己的技术 Error
- 阻止外部模型不受控制地渗透进应用
Infrastructure 不应该:
- 实现 Business Rule
- 做业务决策
- 生成业务派生
- 为具体 View 构建 ViewModel
- 管理 UI State
- 控制 Navigation
- 打开 Dialog
- 承担表现层职责
- 仅仅因为“代码现在就在某个技术 Service 里”,就继续把其他逻辑塞进来
如果 Infrastructure Layer 里的 Business Logic 越来越“有意思”,就值得认真检查:这些逻辑是否真的属于这里。
什么时候它会变得更复杂
Section titled “什么时候它会变得更复杂”Customer 例子中的简单 Mapper,并不是 Eric Evans 在 Domain-Driven Design 中所说的完整 Anti-Corruption Layer。
但两者共享同一个防御性原则:外部模型不应该不受控制地成为自己的模型。
如果只是把 first_name 转成 firstName,这条边界确实非常普通。
当两边开始使用不同术语、不同状态模型、甚至不同业务含义时,事情才真正复杂起来。也许外部系统的 "ACTIVE" 与我们 Feature 里的 Active 并不是同一个含义。也许 Legacy System 返回某些 Error Code,而这些 Code 只在那个系统里有意义。也许两个系统都在建模“Customer”,但从业务上讲的其实并不是同一个概念。
到了这里,机械字段映射就不再足够。
这种更深层的外部模型与内部模型之间的转换,会在 Anti-Corruption Layer 中详细讨论。
对 Infrastructure Layer 来说,目前只需要保留一个更简单的结论:即使是一条很小、显式的边界,也能防止外部模型悄悄穿过整个应用。
需要多少边界就做多少,但 Infrastructure 越少越好
Section titled “需要多少边界就做多少,但 Infrastructure 越少越好”不是每个 Primitive Field 都需要独立 Value Object。也不是每个 API Object 都必须配一个 Interface、Repository、Port、Adapter、Factory 和几层抽象。
Layering 不是比赛谁的文件更多。
一个非常简单的 Contract 也不一定需要重复建模。真正重要的是:直接使用它会带来多大风险。
可以问这些问题:
- 这个 Contract 属于谁?
- 它能否在我们无法控制的情况下变化?
- 它会深入应用多少层?
- 外部与内部之间是否存在不同语义?
- 数据是否需要标准化或验证?
- 未来再解耦会有多贵?
一个只在局部使用的小型技术 Lookup Structure,也许完全不值得再加一层抽象。
但一个以后会影响业务决策的支付状态,即使最初转换只有几行,也值得有意识地设置边界。
区别不在代码行数。
而在于:如果没有这条边界,会产生多少耦合。
边界只做到必要程度,Infrastructure 只保留必要内容。
Infrastructure 完全可以继续很无聊。
从这里开始进入 +State
Section titled “从这里开始进入 +State”走到 Infrastructure Layer 的末端时,我们已经接收、校验、标准化了外部数据,并把它们转换成 Feature 可以信任的形式。
但还没有做任何业务决策。
"ACTIVE" 已经变成 CustomerStatus.Active。可这个客户是否允许下单、订单能否取消、当前 State Transition 是否允许,都还没有答案。
当这些数据跨过技术边界之后会发生什么,就是 +State 开始回答的问题。
业务逻辑从那里开始。