088
architectural DDD

Hexagonal Architecture

Reference Wikipedia ↗
Hexagonal Architecture — class diagram
Plate 088 class diagram

The Hexagonal Architecture (also known as Ports and Adapters) is a software design pattern that aims to create loosely coupled software applications with a clear separation of concerns. The core business logic is kept independent of external technologies like databases, UI frameworks, or messaging systems. This is achieved by defining “ports” (interfaces) that represent interactions with the outside world and “adapters” that implement these ports for specific technologies.

Essentially, the application’s core doesn’t know about the external world; it only interacts through these well-defined ports. This makes the core logic highly testable, maintainable, and adaptable to changes in external dependencies. Adapters translate the core’s requests into the language of the external system and vice-versa. This pattern promotes testability by allowing you to easily mock or stub external dependencies during testing.

Usage

Hexagonal Architecture is commonly used in:

  • Complex Business Logic: Applications with substantial domain logic benefit greatly from the clear separation of concerns.
  • Microservices: The pattern’s focus on isolation aligns well with the microservices approach.
  • Long-Lived Applications: Where requirements and external technologies are likely to evolve over time.
  • Test-Driven Development: The clear interfaces facilitate easy unit and integration testing.
  • Systems Requiring Flexibility: When you anticipate needing to switch databases, UI frameworks, or integrate with various external systems.

Examples

  • Spring Boot (Java): Spring’s dependency injection and interface-based programming naturally lend themselves to Hexagonal Architecture. You can define interfaces for repositories (ports) and then provide different implementations (adapters) for different databases (e.g., JPA, MongoDB). Spring Data REST further simplifies creating APIs that interact with these ports.
  • NestJS (Node.js): NestJS, a progressive Node.js framework, encourages the use of modules and providers, which can be structured to implement the Ports and Adapters pattern. Services define the core logic and interact with repositories (ports) through interfaces. Different database technologies can be plugged in as adapters to these repository interfaces.
  • Laravel (PHP): While not strictly enforced, Laravel’s service container and interface-based contracts allow developers to implement Hexagonal Architecture. Repositories can be defined as interfaces, and different database implementations can be bound to those interfaces. Event dispatching can be used to represent domain events.

Specimens

15 implementations
Specimen 088.01 Dart View specimen ↗

The Hexagonal Architecture (Ports and Adapters) aims to create loosely coupled software by isolating the core application logic from external concerns like databases, UI frameworks, or message queues. This is achieved through defining ports (interfaces) that the core application uses to interact with the outside world, and adapters that implement these ports to connect to specific technologies. Our Dart example demonstrates this by separating a UserRepository port from its FirebaseUserAdapter implementation, allowing the core UserService to remain independent of Firebase. The use of interfaces and dependency injection promotes testability and flexibility. This style is very idiomatic to Dart because of its strong support for interfaces and is often integrated with dependency injection frameworks like get_it.

// core_domain/user_service.dart
abstract class UserService {
  Future<String> getUserName(int userId);
}

// core_domain/user_repository.dart
abstract class UserRepository {
  Future<String> fetchUserName(int userId);
}

// infrastructure/firebase_user_adapter.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import '../core_domain/user_repository.dart';

class FirebaseUserAdapter implements UserRepository {
  final FirebaseDatabase _database = FirebaseDatabase.instance;

  @override
  Future<String> fetchUserName(int userId) async {
    final snapshot = await _database.ref('users/$userId').child('name').get();
    return snapshot.value as String? ?? 'Unknown User';
  }
}

// application/user_service_implementation.dart
import '../core_domain/user_service.dart';
import '../core_domain/user_repository.dart';

class UserServiceImpl implements UserService {
  final UserRepository _userRepository;

  UserServiceImpl(this._userRepository);

  @override
  Future<String> getUserName(int userId) async {
    return _userRepository.fetchUserName(userId);
  }
}

// main.dart
import 'package:firebase_core/firebase_core.dart';
import 'application/user_service_implementation.dart';
import 'infrastructure/firebase_user_adapter.dart';

async void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  final userRepository = FirebaseUserAdapter();
  final userService = UserServiceImpl(userRepository);

  final userName = await userService.getUserName(123);
  print('User Name: $userName');
}