开发
软件开发相关知识
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人以上团队协作的复杂业务系统中收益明显,中小项目建议先用简单分层。