正在加载,请稍候…

Software Architecture Patterns: Layered, Hexagonal, and Clean

Compare and implement layered, hexagonal, and clean architecture patterns. Choose the right architecture for your project's complexity and scale.

Software Architecture Patterns: Layered, Hexagonal, and Clean

Architecture defines how your code is organized and how dependencies flow.

Layered Architecture

Presentation Layer  → Controllers, API handlers
Business Layer     → Services, use cases
Data Layer         → Repositories, ORM
Infrastructure     → Database, external services
// Layered example
// Layer 1: Controller (Presentation)
class UserController {
  constructor(private userService: UserService) {}

  async createUser(req: Request, res: Response) {
    const user = await this.userService.create(req.body);
    res.status(201).json(user);
  }
}

// Layer 2: Service (Business)
class UserService {
  constructor(private userRepo: UserRepository) {}

  async create(data: CreateUserDto): Promise<User> {
    await this.validateEmail(data.email);
    return this.userRepo.save(new User(data));
  }
}

// Layer 3: Repository (Data)
class UserRepository {
  async save(user: User): Promise<User> {
    return this.db.users.create(user);
  }
}

Hexagonal Architecture (Ports & Adapters)

Isolates the domain from infrastructure concerns.

// Domain Core (no external dependencies)
class UserService {
  constructor(
    private userPort: UserPort,       // port (interface)
    private emailPort: EmailPort      // port (interface)
  ) {}

  async register(email: string, password: string): Promise<User> {
    const exists = await this.userPort.findByEmail(email);
    if (exists) throw new DuplicateEmailError();
    const user = User.create(email, password);
    await this.userPort.save(user);
    await this.emailPort.sendWelcome(email);
    return user;
  }
}

// Port (interface defined by domain)
interface UserPort {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<void>;
}

interface EmailPort {
  sendWelcome(email: string): Promise<void>;
}

// Adapters (implement ports, depend on external libs)
class PostgresUserAdapter implements UserPort {
  constructor(private db: PrismaClient) {}

  async findByEmail(email: string) {
    return this.db.user.findUnique({ where: { email } });
  }

  async save(user: User) {
    await this.db.user.upsert({ where: { id: user.id }, create: user, update: user });
  }
}

class SendGridEmailAdapter implements EmailPort {
  async sendWelcome(email: string) {
    await sendGrid.send({ to: email, subject: 'Welcome!', text: '...' });
  }
}

Clean Architecture

// Entities - enterprise business rules
class User {
  private constructor(
    readonly id: string,
    readonly email: Email,
    readonly createdAt: Date
  ) {}

  static create(email: string): User {
    return new User(crypto.randomUUID(), Email.of(email), new Date());
  }
}

// Use Cases - application business rules
class RegisterUserUseCase {
  constructor(
    private userRepo: UserRepository,
    private emailService: EmailService,
    private eventBus: EventBus
  ) {}

  async execute(request: RegisterUserRequest): Promise<RegisterUserResponse> {
    const existing = await this.userRepo.findByEmail(request.email);
    if (existing) return { success: false, error: 'Email already registered' };

    const user = User.create(request.email);
    await this.userRepo.save(user);
    await this.emailService.sendWelcome(user.email.toString());
    await this.eventBus.publish(new UserRegistered(user.id));

    return { success: true, userId: user.id };
  }
}

// Interface Adapters - controllers, presenters, gateways
class RegisterUserController {
  constructor(private useCase: RegisterUserUseCase) {}

  async handle(req: Request, res: Response) {
    const result = await this.useCase.execute({ email: req.body.email });
    if (!result.success) return res.status(400).json({ error: result.error });
    res.status(201).json({ id: result.userId });
  }
}

When to Use Each

Pattern Best For
Layered Simple CRUD apps, small teams
Hexagonal Multiple adapters (CLI + HTTP + events), high testability
Clean Complex domains, long-lived systems

Architecture is a tool, not a dogma—choose based on your context.