Skip to content

Angular Hexagonal Architecture

Hexagonal Architecture, also known as Ports and Adapters pattern or Clean Architecture, is an architectural pattern that allows an application to be equally driven by users, programs, automated tests, or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases.

Core Concepts

The hexagonal architecture pattern is based on three main principles:

  1. Explicit separation of concerns through layered architecture
  2. Dependencies pointing inwards - outer layers depend on inner layers
  3. Isolation of business rules in the domain layer

Architecture Layers

1. Domain Layer (Core)

The innermost layer containing:

  • Business logic
  • Domain models/entities
  • Domain services
  • Interfaces (ports) that define how the domain interacts with outside layers

2. Application Layer

The layer that orchestrates the flow of data between the outer layers and the domain:

  • Use cases/application services
  • DTOs (Data Transfer Objects)
  • Interface adapters
  • State management

3. Infrastructure Layer

The outermost layer handling external concerns:

  • API clients
  • Database adapters
  • Framework-specific code
  • UI Components

Implementation in Angular

Project Structure

A typical Angular hexagonal architecture project structure:

src/
├── app/
│   ├── core/              # Domain layer
│   │   ├── models/        # Domain entities
│   │   ├── ports/         # Interface definitions
│   │   └── services/      # Domain services
│   │
│   ├── application/       # Application layer
│   │   ├── services/      # Use cases
│   │   ├── state/         # State management
│   │   └── facades/       # Interface adapters
│   │
│   ├── infrastructure/    # Infrastructure layer
│   │   ├── api/          # API clients
│   │   ├── storage/      # Storage adapters
│   │   └── ui/           # UI components
│   │
│   └── shared/           # Shared utilities and components

Example Implementation

  1. Domain Layer (Core):
// core/ports/user.port.ts
export interface IUserPort {
  getUser(id: string): Promise<User>;
  updateUser(user: User): Promise<void>;
}

// core/models/user.model.ts
export class User {
  constructor(
    public id: string,
    public name: string,
    public email: string
  ) {}
}

// core/services/user.service.ts
export class UserService {
  constructor(private userPort: IUserPort) {}

  async updateUserProfile(userId: string, name: string): Promise<void> {
    const user = await this.userPort.getUser(userId);
    user.name = name;
    await this.userPort.updateUser(user);
  }
}
  1. Application Layer:
// application/services/user-facade.service.ts
@Injectable()
export class UserFacadeService {
  constructor(private userService: UserService) {}

  async updateProfile(userId: string, name: string): Promise<void> {
    try {
      await this.userService.updateUserProfile(userId, name);
    } catch (error) {
      // Handle application-level errors
    }
  }
}
  1. Infrastructure Layer:
// infrastructure/api/user-api.adapter.ts
@Injectable()
export class UserApiAdapter implements IUserPort {
  constructor(private http: HttpClient) {}

  async getUser(id: string): Promise<User> {
    const response = await this.http.get(`/api/users/${id}`).toPromise();
    return new User(response.id, response.name, response.email);
  }

  async updateUser(user: User): Promise<void> {
    await this.http.put(`/api/users/${user.id}`, user).toPromise();
  }
}

Benefits and Advantages

  1. Framework Independence
  2. Business logic is isolated from Angular framework
  3. Easier to migrate to new versions or different frameworks
  4. Core domain can be shared between different applications

  5. Testability

  6. Domain logic can be tested without UI or external dependencies
  7. Easy to mock external dependencies through ports
  8. Clear separation makes unit testing simpler

  9. Maintainability

  10. Clear boundaries between layers
  11. Changes in one layer don't affect others
  12. Easier to understand and modify business logic

  13. Scalability

  14. New features can be added without modifying existing code
  15. Easy to add new adapters for different data sources
  16. Multiple UI implementations can share same domain logic

Challenges and Considerations

  1. Initial Complexity
  2. More boilerplate code compared to traditional approaches
  3. Steeper learning curve for team members
  4. Additional planning required for layer separation

  5. Development Overhead

  6. Need to maintain strict boundaries between layers
  7. More interfaces and abstractions to manage
  8. May seem over-engineered for simple applications

  9. Performance Considerations

  10. Additional layers may impact performance
  11. Need to carefully manage state across layers
  12. More complex dependency injection setup

Best Practices

  1. Layer Isolation
  2. Keep domain logic pure and framework-agnostic
  3. Use interfaces (ports) to define layer boundaries
  4. Avoid circular dependencies between layers

  5. State Management

  6. Use facades to abstract state management
  7. Keep state close to where it's needed
  8. Consider using observable patterns for reactivity

  9. Testing Strategy

  10. Write unit tests for domain logic first
  11. Use test doubles (mocks/stubs) for external dependencies
  12. Integration tests for adapter implementations

  13. Error Handling

  14. Define domain-specific errors in core layer
  15. Transform technical errors to domain errors in adapters
  16. Handle UI-specific error presentation in infrastructure layer

When to Use

Hexagonal Architecture is particularly beneficial for:

  • Large enterprise applications
  • Applications with complex business logic
  • Systems requiring high maintainability
  • Projects expecting framework migrations
  • Applications needing multiple UI implementations

For simpler applications or quick prototypes, a traditional layered architecture might be more appropriate.