NestJS 架构设计系列:应用服务与领域服务的区别

引言

在上一篇文章《NestJS 架构设计:5 分钟抓住 DDD 的命脉》中,我们介绍了 DDD (领域驱动设计)的经典分层架构。其中,应用层与领域层分别对应两类关键服务:

  • 应用服务(Application Service / Usecase):位于应用层,负责流程编排;
  • 领域服务(Domain Service):位于领域层,承载核心业务规则。

简单来说:应用服务表达用户想做什么,领域服务表达业务该怎么做。

以创建订单为例:

  • 应用服务:用户点击结算 → 计算金额 → 发起支付 → 创建订单 → 通知用户;
  • 领域服务:金额计算、支付处理、订单状态变更等具体业务规则。

应用服务负责串联整个用例流程 ,而领域服务则专注于每个环节的业务实现

应用服务(Usecase)

应用服务是表达具体业务场景(如取消订单、创建支付)的主要载体,它对应用户发起的一个完整操作流程。

核心职责

应用服务本质上是一个协调器,其职责包括:

  • 协调多个领域对象与领域服务;
  • 控制业务用例的执行顺序;
  • 组装并返回最终结果。

尽管应用层不应包含核心业务逻辑,但它可以合理处理以下非核心但必要的横切关注点:权限校验、事务控制、结果组装、发送通知(邮件、消息等非核心业务)

代码示例

我们来看一个真实 NestJS 项目中的应用服务 CancelOrderUsecase

js 复制代码
import { OrderStatus } from '@/modules/payment/client/constant/Order';
// ... 省略其他 imports

@Injectable()
export class CancelOrderUsecase {
  @Inject()
  private readonly dataAssembler: DataAssembler;

  @Inject()
  private readonly cancelOrderByOrderIdService: CancelOrderByOrderIdService;

  @Inject()
  private readonly getOrderByOrderIdService: GetOrderByOrderIdService;

  @Inject()
  private readonly validatePaymentRelatedEntityPrivilegeService: ValidatePaymentRelatedEntityPrivilegeService;

  @Inject()
  private readonly stripeService: StripeService;
  
  async execute(orderId: number, userId: number) {
    // 1. 获取订单信息
    const order = await this.getOrderByOrderIdService.execute(orderId);

    // 2. 状态校验
    if (order.status === OrderStatus.Pending) throw ExceptionFactory.bizException(ErrorCode.INVALID_PARAM);

    // 3. 权限验证
    await this.validatePaymentRelatedEntityPrivilegeService.execute({
      entityId: order.entityId,
      entityType: order.entityType,
      userId,
    });

    // 4. 调用领域服务执行核心业务
    const cancelResult = await this.cancelOrderByOrderIdService.execute(orderId);

    // 5. 处理第三方服务(非核心业务)
    if (this.configService.bizConfig.isGlobalEdition && order.paymentInfo.invoice) {
      try {
        await this.stripeService.voidInvoice(order.paymentInfo.invoice.id);
      } catch (error) {
        // 第三方调用失败不影响主流程
      }
    }

    // 6. 发送通知(非核心业务)
    this.sendNotification(order, userId);

    // 7. 组装返回结果
    const result = await this.dataAssembler.assemble<EntityDTO<Order>>({
      domainObject: cancelResult,
    });

    return SingleResponse.of(result);
  }
}

从以上代码我们可以知道应用服务的特点:

  • 不包含业务逻辑 :订单取消的核心逻辑由 CancelOrderByOrderIdService 领域服务实现;
  • 编排多个服务:依次调用查询、验证、取消等服务;
  • 处理非核心业务:发送通知、调用 Stripe 第三方 API;
  • 组装返回结果 :通过 DataAssembler 将领域对象转换为 DTO。

领域服务(Domain Service)

并非所有业务逻辑都能自然地归属到某个实体值对象 中。当一个操作涉及多个聚合、需要协调多个领域对象,或本身是无状态的业务规则时,我们就将其抽为领域服务

它是领域层的重要组成部分,专注于实现核心业务逻辑,但仅限于那些不属于任何单一领域对象的场景。

核心原则

领域服务的设计遵循以下核心原则:

  • 单一职责:每个服务专注于一个具体的业务操作;
  • 无状态:状态由领域对象(如实体 Entity)保存,领域服务本身不持有状态;
  • 可复用性:可以被多个应用服务调用以执行相同的业务逻辑;
  • 纯粹性:只处理核心业务逻辑,避免混入非业务相关的代码。

什么时候使用领域服务?

  • 当需要执行一个显著的业务操作过程时;
  • 对领域对象进行转换或计算;
  • 当业务逻辑涉及多个领域对象,并且需要返回一个值对象作为结果时。

代码示例

我们同样来看一个真实 NestJS 项目中的领域服务 TransferUserCreditToTeamService(用户积分转团队):

js 复制代码
async execute({
  userId,
  teamId,
  transactionId,
  userCreditHistoryData,
  teamCreditHistoryData,
  amount,
}: {
  userId: number;
  teamId: number;
  transactionId: string;
  amount: number;
  userCreditHistoryData: Record<string, unknown>;
  teamCreditHistoryData: Record<string, unknown>;
}) {
  // 开启事务前的准备工作
  const knex = this.teamCreditRepository.getKnex();
  await this.validateRedisLock(teamId);
  await this.teamCreditRepository.findOrCreateOne(teamId);
  const userCredit = await this.userCreditRepository.findOrCreateOne(userId);
  
  // 业务规则校验:余额是否足够
  if (userCredit.amount < amount) {
    throw Error(`user ${userId} credit not enough`);
  }
  
  // 创建 Stripe 余额记录
  const balanceTransactionId = await this.createStripeCustomerBalance(...);
  
  try {
    await knex.transaction(async (trx) => {
      // 事务内的业务逻辑:
      // 1. 检查流水是否已存在(幂等性)
      // 2. 扣减用户积分
      // 3. 增加团队积分
      // 4. 校验最终状态
    });
  } catch (error) {
    throw error;
  }
  
  // 发布领域事件
  await this.domainEventPublisher.publishSync(
    new UserCreditHistoryCreatedEvent(...)
  );
  await this.domainEventPublisher.publishSync(
    new TeamCreditHistoryCreatedEvent(...)
  );
}

这个领域服务具体展示了以下几个方面:

  • 业务规则:如余额校验、流水幂等性检查;
  • 事务处理:确保转账过程中的原子性,保障数据一致性;
  • 领域事件:过发布领域事件通知其他聚合关于积分变动的信息。

实体(Entity)与领域服务的职责划分

在前面介绍中,我们提到了实体(Entity)和值对象 ,在 DDD 中,业务逻辑应优先内聚在实体或值对象中 。只有当某个行为无法自然归属于单一实体(例如涉及多个聚合、无状态操作、或跨对象协作)时,才将其提取为领域服务

那么,具体如何判断一段逻辑该放在哪里?可以从以下几个维度进行权衡:

维度 实体(Entity) 领域服务(Domain Service
状态依赖 逻辑强依赖于自身状态(如实例属性) 无状态,不持有数据,仅基于输入参数执行操作
作用范围 仅操作当前实体实例 通常涉及多个实体、聚合或值对象
聚合边界 不应跨越聚合边界(即不直接操作其他聚合根) 可协调多个聚合之间的交互(但需谨慎)多个领域对象
内聚性 行为是该实体天然具备的能力(如 order.cancel()) 行为是外部赋予的协作逻辑(如 transferCredit(user, team))

来看一个真实 NestJS 项目中的 Order 实体中的行为:

js 复制代码
@Property({ persist: false })
get paymentInfo(): typeof this._paymentInfo {
  try {
    const databaseValue =
      typeof this._paymentInfo === 'string'
        ? JSON.parse(this._paymentInfo)
        : this._paymentInfo;
    const clone = cloneDeep(databaseValue);

    // 基于当前订单 ID 生成预览路径(强依赖 this.id)
    if (
      clone.credential?.receiptFiles?.length > 0
    ) {
      clone.credential.receiptFiles = clone.credential.receiptFiles.map((item) =>
        getCredentialImagePreviewPath(String(this.id), item)
      );
    }

    return clone;
  } catch {
    return {};
  }
}

set paymentInfo(value) {
  const parsed = safeJsonParse(value, {});

  // 清理路径前缀(同样依赖 this.id)
  if (parsed.credential?.receiptFiles?.length > 0) {
    parsed.credential.receiptFiles = parsed.credential.receiptFiles.map((item) =>
      removeCredentialImagePrefix(String(this.id), item)
    );
  }

  this._paymentInfo = parsed;
}

这段逻辑:

  • 完全基于 this.idthis._paymentInfo 运行;
  • 不调用其他聚合或服务;
  • 是对自身持久化数据的视图转换,而非业务决策。

因此,它非常适合放在实体内部,既保持了内聚性,又避免了不必要的服务膨胀。

最后,应用服务领域服务 的区别已介绍完毕,阅读本篇文章最好结合上篇文章《NestJS 架构设计:5 分钟抓住 DDD 的命脉》一起阅读,可能会更好理解,如果你觉得有什么地方遗漏、疑惑,欢迎评论讨论。

相关推荐
技术不打烊2 小时前
MySQL主从延迟飙升?元数据锁可能是“真凶”
后端
沉迷技术逻辑2 小时前
微服务保护和分布式事务
分布式·微服务·架构
無量2 小时前
MySQL架构原理与执行流程
后端·mysql
MarkHD2 小时前
智能体在车联网中的应用:第11天 CARLA自动驾驶仿真入门:从零安装到理解客户端-服务器架构
服务器·架构·自动驾驶
JHC0000002 小时前
dy直播间评论保存插件
java·后端·python·spring cloud·信息可视化
野蛮人6号2 小时前
黑马微服务 p23Docker02 docker的安装 如何正确安装docker,黑马微服务给的文档不行了,如何正确找到解决方法
java·docker·微服务·架构
小毅&Nora2 小时前
【后端】【微服务网关】 ① 全景图:2025年主流网关选型、原理与实战指南
网关·微服务·架构
Wnq100722 小时前
新型基于“去中心化分布式Agent“技术的操作系统DIOS
分布式·嵌入式硬件·中间件·架构·云计算·去中心化·信息与通信
武子康3 小时前
大数据-190 Filebeat→Kafka→Logstash→Elasticsearch 实战
大数据·后端·elasticsearch