开发

软件开发相关知识

Node.js 22企业级架构:DDD+CQRS模式在高并发业务系统中的落地实践

为什么企业级Node.js需要DDD+CQRS?

随着业务规模增长,"Controller直接调Service再调DB"的三层架构会演变成意大利面条代码。 DDD+CQRS不是过度设计,而是应对复杂业务逻辑的有效工具。


一、项目结构(DDD分层)

src/
  domain/                 # 领域层(核心业务逻辑,不依赖任何框架)
    user/
      User.ts             # 领域实体
      UserRepository.ts   # 仓储接口(端口)
      UserEvents.ts       # 领域事件
      commands/
        CreateUserCommand.ts
        handlers/
          CreateUserCommandHandler.ts
      queries/
        GetUserQuery.ts
        handlers/
          GetUserQueryHandler.ts

  infrastructure/         # 基础设施层(数据库、消息队列等)
    persistence/
      PostgresUserRepository.ts  # 仓储实现(适配器)
    messaging/
      KafkaEventBus.ts

  application/            # 应用层(用例编排)
    UserApplicationService.ts

  presentation/           # 接口层(HTTP/GraphQL/gRPC)
    http/
      UserRouter.ts

二、领域实体设计

// domain/user/User.ts
import { AggregateRoot } from '../shared/AggregateRoot';
import { UserCreatedEvent } from './UserEvents';

interface UserProps {
  id: string;
  email: Email;
  passwordHash: string;
  status: UserStatus;
  createdAt: Date;
}

export class User extends AggregateRoot<UserProps> {
  // 工厂方法:通过静态方法创建,保证不变性
  static create(email: string, password: string): User {
    const emailVO = Email.create(email);  // 值对象负责校验
    const passwordHash = bcrypt.hashSync(password, 12);

    const user = new User({
      id: generateId(),
      email: emailVO,
      passwordHash,
      status: UserStatus.ACTIVE,
      createdAt: new Date(),
    });

    // 领域事件:用于后续的副作用处理
    user.addDomainEvent(new UserCreatedEvent(user.id, email));
    return user;
  }

  changeEmail(newEmail: string): void {
    const email = Email.create(newEmail);
    if (this.props.email.equals(email)) {
      throw new DomainError('新邮箱与当前邮箱相同');
    }
    this.props.email = email;
    this.addDomainEvent(new UserEmailChangedEvent(this.id, newEmail));
  }
}

三、CQRS:命令和查询分离

// 命令:改变状态
// domain/user/commands/CreateUserCommand.ts
export class CreateUserCommand {
  constructor(
    public readonly email: string,
    public readonly password: string,
    public readonly name: string,
  ) {}
}

// domain/user/commands/handlers/CreateUserCommandHandler.ts
@injectable()
export class CreateUserCommandHandler
  implements ICommandHandler<CreateUserCommand, UserDto> {

  constructor(
    @inject(UserRepository) private userRepo: UserRepository,
    @inject(EventBus) private eventBus: EventBus,
  ) {}

  async execute(command: CreateUserCommand): Promise<UserDto> {
    // 业务规则检查
    const existing = await this.userRepo.findByEmail(command.email);
    if (existing) throw new ConflictError('邮箱已存在');

    const user = User.create(command.email, command.password);
    await this.userRepo.save(user);

    // 发布领域事件(发送欢迎邮件等副作用)
    await this.eventBus.publishAll(user.getDomainEvents());

    return UserDto.fromDomain(user);
  }
}

// 查询:只读,走独立的读模型(可以接数据库只读副本)
export class GetUserQueryHandler {
  constructor(private readonly readDb: ReadDatabase) {}

  async execute(query: GetUserQuery): Promise<UserDetailDto | null> {
    return this.readDb.users.findOne({
      where: { id: query.userId },
      select: ['id', 'email', 'name', 'createdAt'],
    });
  }
}

四、Fastify路由层接入

// presentation/http/UserRouter.ts
import Fastify from 'fastify';
import { container } from '../container';

const fastify = Fastify({
  logger: { level: 'info' },
  // Node.js 22原生支持的权限模型
  trustProxy: true,
});

// Schema验证(Fastify内置,性能优于express+joi)
const createUserSchema = {
  body: {
    type: 'object',
    required: ['email', 'password'],
    properties: {
      email: { type: 'string', format: 'email' },
      password: { type: 'string', minLength: 8 },
    },
  },
  response: {
    201: { $ref: 'UserDto#' },
  },
};

fastify.post('/users', { schema: createUserSchema }, async (req, reply) => {
  const handler = container.get(CreateUserCommandHandler);
  const user = await handler.execute(new CreateUserCommand(
    req.body.email,
    req.body.password,
    req.body.name,
  ));
  reply.status(201).send(user);
});

五、Node.js 22新特性

// 原生SQLite(无需sqlite3 npm包)
import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');

// 原生权限模型(实验性)
// node --experimental-permission --allow-fs-read=./data app.js

// 原生glob
const { glob } = require('node:fs/promises');
const files = await glob('src/**/*.ts');

DDD+CQRS在100人以上团队协作的复杂业务系统中收益明显,中小项目建议先用简单分层。