架构笔记:Service 逻辑解耦与 Domain Interface 落地

1. 问题起因:Service 承担了太多转换逻辑

目前后端代码比较突出的问题是 Service 层里 Transform (Mapper) 偏多。Service 本应聚焦业务步骤,但现在需要处理各种类型转换:

  • Adapter 接口定义不清:比如 Service 在调用第三方 Client 拼 SOAP 报文时,新增一个 Adapter,它的入参应该用什么类型?是用第三方的格式,还是用我们自己定义的 Domain Model?

  • Domain Model 缺失

    • Entity 绑定 DB 结构,不适合充当 Domain Model。

    • DTO/Types 混合了前端请求和临时定义,也不适合充当 Domain Model。

  • 现状:缺少统一的业务模型,导致 Service 成了类型转换的汇集点。

2. DDD 的思路:以业务模型为中心

按照 DDD 的思路,基础设施层应适配业务模型(Domain Model):

  • 第三方 Adapter:应依赖 Domain Model,负责将我们的标准数据转换为第三方接口要求的格式。

  • Repository:应接收 Domain Model。Service 不需要负责构建 Entity,存取逻辑交由 Repo 内部处理。

  • Controller:在入口处将 DTO 转换为 Domain Model。

3. DDD 落地难题:技术与现实的双重阻力

A. 技术层面的“手动挡”痛点 (LB4 vs. Java)

  • 级联与脏检查缺失:Java 的 Hibernate 支持 Dirty Checking,开发者修改 Model 属性后,框架会自动同步到多张表。LB4 下需要手动处理 patch

  • 加载策略差异:Java 支持 Lazy Loading(延迟加载),可以定义大而全的聚合根且不影响性能;LB4 需要显式 include,否则就得在 Service 里写零散的查询。

  • 持久化细节溢出:LB4 强制一表一 Repo,导致更新聚合根时的事务编排、Diff 对比都堆积在 Service 里。

B. 现实层面的“深层阻力”(核心痛点)

  • 认知惯性与成本:团队习惯了 CRUD 的开发方式。引入 Domain Interface 意味着多写一层转换(Mapper),在交付压力大的时候,容易被看作“浪费时间”。

  • “改不动”的恐惧:老代码里 DTO、Entity 和业务逻辑深度耦合。要把这些混杂的 types 拆分出来,往往牵一发动全身,很难保证不出问题。

  • 收益隐形化:解耦带来的收益(易维护、测试友好)是长期的,而重构的成本(工时、回归测试)是眼前的。在“业务优先”的环境下,这类改动不容易争取到资源支持。

4. 最小改动方案:渐进式落地 (Touch-and-Go)

由于阻力较大,暂时不做大范围的架构升级,先采取以下务实步骤:

  • 第一步:确立主权 (Domain Interface)
    从现有 types 中提炼出标准的 Domain Interface。老逻辑暂不改动,先给新功能设定规范。

  • 第二步:建立防腐层 (Adapter & Mapper)
    针对痛点较明显的第三方服务(如 SOAP),强制其 Adapter 依赖 Domain Interface。把字段映射这类枯燥的工作从 Service 中移出去。

  • 第三步:持久化下沉 (Smart Repository)
    在 Repository 层封装多表操作。Service 只需调用 save\(domainInterface\),内部处理具体细节,减少 Service 的视觉干扰。

  • 第四步:增量渗透
    遵循 Touch-and-Go 原则:改到哪,修到哪。不为了重构而重构,而是在业务变动的间隙,顺带完成解耦。

5. 最终总结:架构主权的“防腐”与务实演进

1. 隔离变化:建立三道防火墙
我们的核心目标不是消除转换,而是通过物理隔离,把“体力活”从业务逻辑中抽离出来:
接口协议变了(DTO) → 只改 Mapper
数据库表结构变了(Entity) → 只改 Repository
核心业务规则变了(Logic) → 只改 Domain Service/Logic

2. 务实主义:承认限制,划出净土
不盲目遵循 DDD 教条(比如非要实现 Java 那种透明异步加载或脏检查),而是针对 LB4 的现状做折中:
手动挡的封装:既然框架没有“全自动”功能,就把多表 Diff 和事务编排这些脏活限制在 Smart Repository 内部,不溢出到 Service。
接口即主权:利用 TypeScript 的 Interface 特性,在不改动底层老旧 Entity 的前提下,定义出一套相对纯净、统一的业务语言。

3. 落地策略:Touch-and-Go(增量渗透)
真正的阻力来自“改不动”的恐惧和交付压力,所以暂时不做大开大合的调整:
改到哪,修到哪:不为了重构而重构。只在业务变动触及相关逻辑时,顺手把旧的映射逻辑移到 Mapper,把第三方报文拼接放进 Adapter。
从局部清爽开始:通过一个个“干净”的 Service 示范,让团队逐渐感受到解耦后维护成本的降低,慢慢从“搬运工”模式转向“指挥官”模式。

《Designing Data Intensive Applications》读书笔记 - 数据库复制

这一章讲数据库复制(Replication),目标很简单就是保存数据副本在多个机器上,但是实现却没那么容易。首先需要数据复制的几个原因:

  1. 数据中心地理上更靠近用户
  2. 增强可用性,即便部分服务器节点失败,整个系统依然可用
  3. 提高读取的吞吐量
阅读更多

《The DynamoDB Book》读书笔记

同事推荐的一本书,只有英文电子版。作者是Alex DeBrie,之前介绍过,是单表设计的推崇者。

这本书前面部分几个章节介绍 DynamoDB 的基本概念,后面部分是一些实际的设计案例。

阅读更多

DynamoDB 单表设计的优势与考量

大多数开发都有关系数据库设计经验,在初次使用 DynamoDB 设计数据模型的时候,很容易陷入关系数据库的思维陷阱, 不自觉的遵守关系数据库设计的范式, 尝试将数据模型规范化,每个实体或实体关系都有对应的单独的表,通常称之为多表设计。
与之对应的是,将所有实体和实体关系都存储在同一张表中,毕竟 DynamoDB 是 Schemaless 的数据库,称之为单表设计。
这儿要强调的是,这两种设计只是极端的两点。可能也不是一个合适的命名,因为在实际应用中,单表设计并不意味着只能有一张表。
在两个极端之间,单表设计更倾向于将相关实体存入在同一张表中,多表设计则倾向将不同实体类型存入不同的表中。

官方文档中,单表和多表设计比较时也较为推荐单表设计。本文就来根据实际经验,讨论下实际实践中单表设计的优势。
我们自己的项目采用的是单表设计,很大程度上受 《The DynamoDB Book》影响,作者 Alex DeBrie 是单表设计的推崇者。当然,我们项目中已经有十几张表,尽管我们已经尽量将相关实体存入同一张表中。

阅读更多

AWS Connect 转接最近通话的客服

需求

最近接到一个需求,需要将客户来电转接到最近与客户通话的客服。这个需求很容易理解,
客户可能因为各种各样的原因中断通话,再次来电很可能是因为同一个诉求,比如保险索赔,可能需要多次来回沟通。
将通话转给同一个客服,客服可以接着继续处理而不用熟悉客户场景,这样做能够提高处理效率。
尽管这个需求看起来很基础,但是并没有一个开箱可用的方案。我们的呼叫中心是 Amazon Connect,不过并没有启用 Profile,一些方案也不能采用。

阅读更多

AWS client getaddrinfo EMFILE issue

最近,在我们系统中引入了 AWS Cloud Map 作为我们的服务发现系统。部署几周后没有问题,今天突然抛出错误,日志显示错误 getaddrinfo EMFILE events.ap-southeast-2.amazonaws.com
当然,并非所有请求都触发了此错误,只是在高流量时段才出现了这个错误。

阅读更多

如何防止重复处理 SQS 消息

问题

一般来说在我们的系统中,消息处理必须保证幂等性,以防止消息重复处理。在我们的系统中,下面两种情况可能导致相同消息被重复处理:

  1. 调度器和消息生产者:调度器或消息生产者可能会被多次触发,比如时不时有些任务因为超时而被多次触发。
  2. 队列管理:如果一个 Lambda 实例处理消息超时,另一个实例可能会在 visibility timeout 设置不合适的情况下得到重新处理相同消息的机会。

如果消息被多次处理,我们可能会向客户发送重复的电子邮件和短信,甚至礼品卡都可能重复发送。所以,我们需要一个通用的机制来确保相同消息不会被多次处理。

阅读更多