Hexagonal Architecture
🔯

Hexagonal Architecture

创建时间
Feb 20, 2025 10:16 AM
Description
标签
作者
本文介绍的是一个NestJS的项目,项目地址为
nestjs-boilerplate
Github
nestjs-boilerplate
Owner
brocoders
Updated
Feb 25, 2025
,开发的时候觉得用的架构模式很不错,在下面详细介绍一下

able of Contents 

Hexagonal Architecture

🧅
六边形架构(Hexagonal Architecture)的最佳实践

Motivation 

The main reason for using Hexagonal Architecture is to separate the business logic from the infrastructure. This separation allows us to easily change the database, the way of uploading files, or any other infrastructure without changing the business logic.

Description of the module structure

 
. ├── domain │ └── [DOMAIN_ENTITY].ts ├── dto │ ├── create.dto.ts │ ├── find-all.dto.ts │ └── update.dto.ts ├── infrastructure │ └── persistence │ ├── document │ │ ├── document-persistence.module.ts │ │ ├── entities │ │ │ └── [SCHEMA].ts │ │ ├── mappers │ │ │ └── [MAPPER].ts │ │ └── repositories │ │ └── [ADAPTER].repository.ts │ ├── relational │ │ ├── entities │ │ │ └── [ENTITY].ts │ │ ├── mappers │ │ │ └── [MAPPER].ts │ │ ├── relational-persistence.module.ts │ │ └── repositories │ │ └── [ADAPTER].repository.ts │ └── [PORT].repository.ts ├── controller.ts ├── module.ts └── service.ts
[DOMAIN ENTITY].ts represents an entity used in the business logic. Domain entity has no dependencies on the database or any other infrastructure.
[SCHEMA].ts represents the database structure. It is used in the document-oriented database (MongoDB).
[ENTITY].ts represents the database structure. It is used in the relational database (PostgreSQL).
[MAPPER].ts is a mapper that converts database entity to domain entity and vice versa.
[PORT].repository.ts is a repository port that defines the methods for interacting with the database.
[ADAPTER].repository.ts is a repository that implements the [PORT].repository.ts. It is used to interact with the database.
infrastructure folder - contains all the infrastructure-related components such as persistenceuploadersenders, etc.
Each component has port and adaptersPort is interface that define the methods for interacting with the infrastructure. Adapters are implementations of the port.

Please provide a detailed introduction (Infrastructure Layer)

. └── infrastructure/ └── persistence/ ├── document/ # MongoDB adapter ├── relational/ # PostgreSQL adapter └── user.repository.ts # Warehouse interface (port)
[user].repository.ts
// infrastructure/persistence/user.repository.ts export abstract class UserRepository { abstract create(data: User): Promise<User>; abstract findById(id: User['id']): Promise<NullableType<User>>; abstract findByEmail(email: User['email']): Promise<NullableType<User>>; // ... }
  • Defined the interface for interacting with the database
  • Define all necessary methods using abstract classes
  • Does not include specific implementation, only defines the contract
Adapters
// infrastructure/persistence/relational/repositories/user.repository.ts @Injectable() export class UsersRelationalRepository implements UserRepository { constructor( @InjectRepository(UserEntity) private readonly usersRepository: Repository<UserEntity>, ) {} async create(data: User): Promise<User> { const persistenceModel = UserMapper.toPersistence(data); const newEntity = await this.usersRepository.save(...); return UserMapper.toDomain(newEntity); } // ... }
  • Implemented specific database operations
  • Using Mapper to Transform between Domain Models and Persistent Models
  • There can be multiple adapters (such as MongoDB and PostgreSQL)

Key design features

Database independence
  • Through the abstract UserRepository interface
  • Can easily switch database implementation (MongoDB/PostgreSQL)
  • Business logic does not rely on specific databases

Mapper mode

const persistenceModel = UserMapper.toPersistence(data); return UserMapper.toDomain(newEntity);
  • Responsible for converting between domain models and persistence models
  • Maintain the purity of domain models

Actual application process

Create User Process
Controller -> Service -> UserRepository (Port) -> Specific Repository Implementation (Adapter) -> database
Data conversion process
Domain User <-> Mapper <-> Database Entity

Recommendations

Don't try to create universal methods in the repository because they are difficult to extend during the project's life. Instead of this create methods with a single responsibility.
// ❌ export class UsersRelationalRepository implements UserRepository { async find(condition: UniversalConditionInterface): Promise<User> { // ... } } // ✅ export class UsersRelationalRepository implements UserRepository { async findByEmail(email: string): Promise<User> { // ... } async findByRoles(roles: string[]): Promise<User> { // ... } async findByIds(ids: string[]): Promise<User> { // ... } }