Hexagonal Architecture
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 implementationsThe 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');
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled application components by isolating the core application logic from external concerns like databases, UI, or messaging systems. This is achieved by defining ports (interfaces) that the core uses to interact with the outside world, and adapters that implement these ports to connect to specific technologies. This allows for swapping implementations without modifying the core.
This Scala example demonstrates a simple Hexagonal Architecture for a user service. The core UserService interacts with a UserRepository port. We provide an in-memory adapter (InMemoryUserRepository) for testing and a potential database adapter (commented out). The User data class represents the domain. The use of traits for ports and case classes/immutability aligns with Scala’s functional strengths and promotes clear interface separation.
trait UserRepository {
def save(user: User): User
def findById(id: String): Option[User]
}
case class User(id: String, name: String)
class UserService(userRepository: UserRepository) {
def createUser(name: String): User = {
val newUser = User(java.util.UUID.randomUUID().toString, name)
userRepository.save(newUser)
}
def getUser(id: String): Option[User] = {
userRepository.findById(id)
}
}
// In-Memory Adapter (for testing)
class InMemoryUserRepository extends UserRepository {
private var users: Map[String, User] = Map.empty
override def save(user: User): User = {
users = users + (user.id -> user)
user
}
override def findById(id: String): Option[User] = {
users.get(id)
}
}
// Potential Database Adapter (implementation omitted for brevity)
// class DatabaseUserRepository extends UserRepository {
// def save(user: User): User = ???
// def findById(id: String): Option[User] = ???
// }
object Main extends App {
val userRepository = new InMemoryUserRepository()
val userService = new UserService(userRepository)
val user1 = userService.createUser("Alice")
val user2 = userService.createUser("Bob")
println(s"Created user: $user1")
println(s"Created user: $user2")
println(s"User with id ${user1.id}: ${userService.getUser(user1.id)}")
println(s"User with id ${user2.id}: ${userService.getUser(user2.id)}")
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by decoupling the core application logic from external concerns like databases, UI, or third-party services. This is achieved by defining ports – interfaces that the core uses to interact with the outside world – and adapters that implement those ports to connect to specific technologies. The core (application and domain) is unaware of the adapters.
This PHP example demonstrates a simple use case: fetching user data. The UserApplication represents the core. The UserRepository port defines how the core interacts with user data. MySQLUserRepository and InMemoryUserRepository are adapters implementing that port, allowing the application to work with either a MySQL database or an in-memory array without modification. Dependency Injection is used to provide the desired adapter to the application. This approach is idiomatic PHP due to its flexible nature and support for interfaces and dependency injection.
<?php
// Domain Model
class User {
public function __construct(public int $id, public string $name) {}
}
// Port: UserRepository Interface
interface UserRepository {
public function getUser(int $id): ?User;
}
// Adapter: MySQL UserRepository
class MySQLUserRepository implements UserRepository {
private $pdo;
public function __construct(\PDO $pdo) {
$this->pdo = $pdo;
}
public function getUser(int $id): ?User {
$stmt = $this->pdo->prepare("SELECT id, name FROM users WHERE id = ?");
$stmt->execute([$id]);
$user = $stmt->fetch(\PDO::FETCH_ASSOC);
if ($user) {
return new User($user['id'], $user['name']);
}
return null;
}
}
// Adapter: InMemory UserRepository
class InMemoryUserRepository implements UserRepository {
private $users = [
1 => new User(1, 'John Doe'),
2 => new User(2, 'Jane Doe'),
];
public function getUser(int $id): ?User {
return $this->users[$id] ?? null;
}
}
// Application Core
class UserApplication {
private $userRepository;
public function __construct(UserRepository $userRepository) {
$this->userRepository = $userRepository;
}
public function getUserDetails(int $id): ?User {
return $this->userRepository->getUser($id);
}
}
// Example Usage (Actuator/UI - minimal for demonstration)
try {
$pdo = new \PDO("mysql:host=localhost;dbname=testdb", 'user', 'password'); // Replace with your MySQL details
$userRepository = new MySQLUserRepository($pdo);
} catch (\PDOException $e) {
// Fallback to InMemory if MySQL is unavailable
$userRepository = new InMemoryUserRepository();
}
$userApp = new UserApplication($userRepository);
$user = $userApp->getUserDetails(1);
if ($user) {
echo "User ID: " . $user->id . "\n";
echo "User Name: " . $user->name . "\n";
} else {
echo "User not found.\n";
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating the core business logic from external concerns like databases, UI, and messaging systems. The core communicates via ports (interfaces), while external systems connect through adapters (implementations of those interfaces). This allows for swapping implementations without impacting the core.
This Ruby example demonstrates a simplified Hexagonal Architecture for a ‘user’ service. The core (UserService) operates on user data defined by a User model. It interacts with a UserRepository port. InMemoryUserRepository and FileUserRepository are adapters providing concrete user storage. A simple CLI adapter exposes functionality. The separation promotes testability and flexibility – switching storage mechanisms requires only adapting the repository. Ruby’s use of duck typing and mixins facilitates defining ports and swapping implementations easily.
# core/user.rb
class User
attr_reader :id, :name
def initialize(id, name)
@id = id
@name = name
end
end
# core/user_service.rb
require_relative 'user'
class UserService
def initialize(user_repository)
@user_repository = user_repository
end
def create_user(name)
user = User.new(rand(1000), name)
@user_repository.save(user)
user
end
def get_user(id)
@user_repository.find(id)
end
end
# ports/user_repository.rb
module UserRepository
def save(user)
raise NotImplementedError
end
def find(id)
raise NotImplementedError
end
end
# adapters/in_memory_user_repository.rb
require_relative '../ports/user_repository'
class InMemoryUserRepository
include UserRepository
def initialize
@users = {}
end
def save(user)
@users[user.id] = user
end
def find(id)
@users[id]
end
end
# adapters/file_user_repository.rb
require_relative '../ports/user_repository'
require 'json'
class FileUserRepository
include UserRepository
def initialize(filepath = 'users.json')
@filepath = filepath
load_users
end
def save(user)
users = load_users
users[user.id] = user.to_h
File.open(@filepath, 'w') { |f| f.write(JSON.pretty_generate(users))}
end
def find(id)
users = load_users
user_data = users[id]
user_data ? User.new(user_data[:id], user_data[:name]) : nil
end
private
def load_users
if File.exist?(@filepath)
JSON.parse(File.read(@filepath), symbolize_names: true)
else
{}
end
end
end
# adapters/cli_adapter.rb
require_relative '../core/user_service'
require_relative '../adapters/in_memory_user_repository' # or FileUserRepository
class CliAdapter
def initialize(user_service)
@user_service = user_service
end
def run
loop do
puts "Choose an action: (1) create user, (2) get user, (3) exit"
action = gets.chomp.to_i
case action
when 1
puts "Enter user name:"
name = gets.chomp
user = @user_service.create_user(name)
puts "Created user with ID: #{user.id}"
when 2
puts "Enter user ID:"
id = gets.chomp.to_i
user = @user_service.get_user(id)
if user
puts "User name: #{user.name}"
else
puts "User not found."
end
when 3
break
else
puts "Invalid action."
end
end
end
end
# main.rb
require_relative 'core/user_service'
require_relative 'adapters/in_memory_user_repository'
require_relative 'adapters/cli_adapter'
repository = InMemoryUserRepository.new # or FileUserRepository.new
user_service = UserService.new(repository)
cli_adapter = CliAdapter.new(user_service)
cli_adapter.run
The Hexagonal Architecture (Ports and Adapters) aims to create loosely coupled software, isolating the core business logic from external concerns like UI, databases, and external services. This is achieved through defining ports (interfaces defining interactions with the core) and adapters (implementations of those ports for specific technologies). Our example focuses on a simple user management domain. The core (UserManager) interacts with UserRepository (a port). Concrete implementations (InMemoryUserAdapter & DatabaseUserAdapter) act as adapters. This promotes testability and allows swapping infrastructure details without modifying core logic. The Swift implementation utilizes protocols for ports, adhering to Swift’s strong typing and interface-oriented approach, allowing for dependency injection and clear separation of concerns.
// MARK: - Core (Domain Logic)
protocol UserRepository {
func getUser(id: String) -> User?
func createUser(user: User)
}
struct User {
let id: String
let name: String
}
class UserManager {
private let userRepository: UserRepository
init(userRepository: UserRepository) {
self.userRepository = userRepository
}
func getUserName(id: String) -> String? {
return userRepository.getUser(id: id)?.name
}
func registerUser(name: String) {
let newUser = User(id: UUID().uuidString, name: name)
userRepository.createUser(user: newUser)
}
}
// MARK: - Adapters (Infrastructure)
class InMemoryUserAdapter: UserRepository {
private var users: [String: User] = [:]
func getUser(id: String) -> User? {
return users[id]
}
func createUser(user: User) {
users[user.id] = user
}
}
class DatabaseUserAdapter: UserRepository {
// Mock database interaction
func getUser(id: String) -> User? {
// Simulate fetching from a database
if id == "123" {
return User(id: "123", name: "Database User")
}
return nil
}
func createUser(user: User) {
// Simulate saving to a database
print("Saving user to database: \(user.name)")
}
}
// MARK: - Client (Application)
// Example Usage
let inMemoryAdapter = InMemoryUserAdapter()
let databaseAdapter = DatabaseUserAdapter()
let userManagerInMemory = UserManager(userRepository: inMemoryAdapter)
userManagerInMemory.registerUser(name: "John Doe")
print(userManagerInMemory.getUserName(id: userManagerInMemory.getUser(id: "any").map { $0.id } ?? "no id"))
let userManagerDatabase = UserManager(userRepository: databaseAdapter)
print(userManagerDatabase.getUserName(id: "123"))
print(userManagerDatabase.getUserName(id: "456"))
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating the core business logic from external concerns like databases, UI, or external services. The core logic interacts with the outside world only through well-defined ports (interfaces). Adapters then implement these ports to translate between the core and the specific external technology. This allows for easy swapping of technologies without impacting the core application.
This Kotlin example showcases a simple Hexagonal Architecture for a user service. The UserService defines the core logic and operates through the UserRepository port. InMemoryUserAdapter and ConsoleOutputPort are adapters. InMemoryUserAdapter provides a concrete implementation for user data storage, and ConsoleOutputPort adapts the core’s output to the console. The implementation utilizes Kotlin’s concise syntax, data classes, and interface-based programming, all common idiomatic practices.
// Core
interface UserRepository {
fun getUserById(id: String): User?
fun saveUser(user: User)
}
data class User(val id: String, val name: String, val email: String)
interface OutputPort {
fun displayUser(user: User)
}
class UserService(private val userRepository: UserRepository, private val outputPort: OutputPort) {
fun getUserAndDisplay(id: String) {
val user = userRepository.getUserById(id)
user?.let { outputPort.displayUser(it) } ?: run { outputPort.displayUser(User("","","User Not Found")) }
}
fun createUser(user: User) {
userRepository.saveUser(user)
}
}
// Adapters
class InMemoryUserAdapter : UserRepository {
private val users = mutableMapOf<String, User>()
override fun getUserById(id: String): User? = users[id]
override fun saveUser(user: User) { users[user.id] = user }
}
class ConsoleOutputPort : OutputPort {
override fun displayUser(user: User) {
println("User ID: ${user.id}, Name: ${user.name}, Email: ${user.email}")
}
}
// Application
fun main() {
val userRepository = InMemoryUserAdapter()
val outputPort = ConsoleOutputPort()
val userService = UserService(userRepository, outputPort)
val newUser = User("123", "Alice", "alice@example.com")
userService.createUser(newUser)
userService.getUserAndDisplay("123")
userService.getUserAndDisplay("456")
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating the core application logic from external concerns like databases, UIs, or external services. The core interacts with the outside world only through ports – interfaces defining how the core needs to interact, not how it interacts. Adapters then implement these ports, translating the core’s needs into the specifics of external technologies. This allows for easy swapping of dependencies and improved testability.
This Rust example demonstrates a simple unit conversion app. The core logic convert_units doesn’t know where the conversion rates come from. It declares a UnitRepository port. InMemoryUnitRepository is an adapter that provides rates directly in code, while potentially a DatabaseUnitRepository could retrieve rates from a database. This aligns with Rust’s strong emphasis on traits (interfaces) and dependency injection allowing for flexibility and testability.
// Core Application Logic (doesn't depend on external details)
pub trait UnitRepository {
fn get_conversion_rate(&self, from: &str, to: &str) -> Result<f64, String>;
}
pub fn convert_units(
amount: f64,
from: &str,
to: &str,
repo: &dyn UnitRepository,
) -> Result<f64, String> {
let rate = repo.get_conversion_rate(from, to)?;
Ok(amount * rate)
}
// Adapters (implement ports to interact with external systems)
pub struct InMemoryUnitRepository {
rates: std::collections::HashMap<&'static str, &'static str>,
}
impl InMemoryUnitRepository {
pub fn new() -> Self {
let mut rates = std::collections::HashMap::new();
rates.insert("meters", "kilometers");
rates.insert("kilometers", "miles");
InMemoryUnitRepository { rates }
}
}
impl UnitRepository for InMemoryUnitRepository {
fn get_conversion_rate(&self, from: &str, to: &str) -> Result<f64, String> {
match (from, to) {
("meters", "kilometers") => Ok(0.001),
("kilometers", "miles") => Ok(0.621371),
(a, b) if a == b => Ok(1.0),
_ => Err("Conversion rate not found".to_string()),
}
}
}
fn main() {
let repo = InMemoryUnitRepository::new();
let result = convert_units(1000.0, "meters", "miles", &repo);
match result {
Ok(miles) => println!("1000 meters is equal to {} miles", miles),
Err(err) => println!("Error: {}", err),
}
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled, testable software by separating the core application logic from external concerns like databases, UI, and message queues. The core defines “ports” – interfaces that external actors interact with. “Adapters” implement these ports, translating external interactions into core-compatible actions and vice versa.
This Go example demonstrates a simple Hexagonal Architecture for a user service. The core package contains the application logic and defines the UserRepository port. The adapters package provides implementations for in-memory and (mock) external user repositories. The cmd package serves as the entry point, using an adapter to interact with the core. The separation allows swapping implementations (e.g., switching databases) without modifying core logic. This aligns with Go’s emphasis on interfaces and composition over inheritance.
// main.go
package main
import (
"fmt"
"hexagonal/core"
"hexagonal/adapters"
)
func main() {
// Choose an adapter (InMemoryRepository in this case)
repo := adapters.NewInMemoryUserRepository()
// Create the user service with the adapter
userService := core.NewUserService(repo)
// Interact with the service
user, err := userService.GetUser(1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("User: %+v\n", user)
updatedUser, err := userService.UpdateUser(user.ID, "New Name")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Updated User: %+v\n", updatedUser)
}
// core/user_service.go
package core
type UserRepository interface {
GetUser(id int) (*User, error)
UpdateUser(user *User) (*User, error)
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.GetUser(id)
}
func (s *UserService) UpdateUser(id int, name string) (*User, error) {
user, err := s.repo.GetUser(id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
user.Name = name
return s.repo.UpdateUser(user)
}
type User struct {
ID int
Name string
}
// adapters/user_repository.go
package adapters
import "fmt"
type InMemoryUserRepository struct {
users map[int]*core.User
}
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{
users: map[int]*core.User{
1: {ID: 1, Name: "Old Name"},
2: {ID: 2, Name: "Another Name"},
},
}
}
func (r *InMemoryUserRepository) GetUser(id int) (*core.User, error) {
user, ok := r.users[id]
if !ok {
return nil, fmt.Errorf("user with id %d not found", id)
}
return user, nil
}
func (r *InMemoryUserRepository) UpdateUser(user *core.User) (*core.User, error) {
r.users[user.ID] = user
return user, nil
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by isolating the core application logic from external concerns like UI, databases, and other infrastructure. It achieves this through ports (interfaces defining interactions with the core) and adapters (implementations of those ports connecting to specific technologies). This example shows a simple application for greetings. The core logic resides in greeting_core.c and only depends on the port interface igreeting_port.h. Adapters in greeting_console_adapter.c and greeting_file_adapter.c provide console and file-based interactions, respectively. This structure makes testing easier and allows swapping implementations without affecting the core. C’s function pointer based interfaces naturally lend themselves well to this pattern.
// igreeting_port.h
#ifndef IGREETING_PORT_H
#define IGREETING_PORT_H
#include <stdio.h>
typedef void (*greeting_callback)(const char *message);
typedef struct {
greeting_callback greet;
void (*close)();
} igreeting_port;
#endif // IGREETING_PORT_H
// greeting_core.h
#ifndef GREETING_CORE_H
#define GREETING_CORE_H
#include "igreeting_port.h"
typedef struct {
igreeting_port *port;
} greeting_service;
greeting_service create_greeting_service(igreeting_port *port);
void process_greeting(greeting_service *service, const char *name);
#endif // GREETING_CORE_H
// greeting_core.c
#include "greeting_core.h"
#include <stdlib.h>
#include <stdio.h>
greeting_service create_greeting_service(igreeting_port *port) {
greeting_service service;
service.port = port;
return service;
}
void process_greeting(greeting_service *service, const char *name) {
char message[100];
snprintf(message, sizeof(message), "Hello, %s!", name);
service->port->greet(message);
}
// greeting_console_adapter.h
#ifndef GREETING_CONSOLE_ADAPTER_H
#define GREETING_CONSOLE_ADAPTER_H
#include "igreeting_port.h"
igreeting_port create_console_port();
#endif // GREETING_CONSOLE_ADAPTER_H
// greeting_console_adapter.c
#include "greeting_console_adapter.h"
#include <stdio.h>
#include <stdlib.h>
static void console_greet(const char *message) {
printf("%s\n", message);
}
static void console_close() {
// Nothing to do for console output
}
igreeting_port create_console_port() {
igreeting_port port;
port.greet = console_greet;
port.close = console_close;
return port;
}
// greeting_file_adapter.h
#ifndef GREETING_FILE_ADAPTER_H
#define GREETING_FILE_ADAPTER_H
#include "igreeting_port.h"
igreeting_port create_file_port(const char *filename);
#endif // GREETING_FILE_ADAPTER_H
// greeting_file_adapter.c
#include "greeting_file_adapter.h"
#include <stdio.h>
#include <stdlib.h>
static void file_greet(const char *message) {
FILE *fp = fopen("greetings.txt", "a");
if (fp != NULL) {
fprintf(fp, "%s\n", message);
fclose(fp);
}
}
static void file_close() {
//No specific cleanup needed in this simple example
}
igreeting_port create_file_port(const char *filename) {
igreeting_port port;
port.greet = file_greet;
port.close = file_close;
return port;
}
// main.c
#include "greeting_core.h"
#include "greeting_console_adapter.h"
#include "greeting_file_adapter.h"
int main() {
igreeting_port console_port = create_console_port();
greeting_service console_service = create_greeting_service(&console_port);
process_greeting(&console_service, "Alice");
igreeting_port file_port = create_file_port("greetings.txt");
greeting_service file_service = create_greeting_service(&file_port);
process_greeting(&file_service, "Bob");
console_port.close();
file_port.close();
return 0;
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating the core application logic from external concerns like databases, UI, and third-party services. The core interacts with the outside world via ports (interfaces defining allowed interactions) which are then implemented by adapters (concrete components connecting to specific technologies). This allows swapping implementations of external concerns without affecting the core.
This example outlines a simple use case: a service that processes user names. The core defines a UserProcessorPort interface. A ConsoleUI adapter provides input/output, and a NameRepository adapter mocks data persistence. This structure exemplifies C++’s strength in interface-based design and promotes testability, as the core logic doesn’t depend on concrete implementations and can be tested with mock adapters. Dependency Injection is used for loose coupling.
#include <iostream>
#include <string>
// Core Application Logic - Define the Port
class UserProcessorPort {
public:
virtual ~UserProcessorPort() = default;
virtual std::string processName(const std::string& name) = 0;
};
// Core Application - Implementation of the use case
class UserProcessor : public UserProcessorPort {
public:
std::string processName(const std::string& name) override {
//Business logic to process the name (example: capitalize)
std::string processedName = name;
if (!processedName.empty()) {
processedName[0] = toupper(processedName[0]);
}
return processedName;
}
};
// External Concern - Data Storage (Port)
class UserRepositoryPort {
public:
virtual ~UserRepositoryPort() = default;
virtual bool saveUser(const std::string& name) = 0;
};
// External Concern - Adapter for Data Storage (Mock Implementation)
class NameRepository : public UserRepositoryPort {
public:
bool saveUser(const std::string& name) override {
std::cout << "Saving user: " << name << " to database (mock)." << std::endl;
return true;
}
};
// External Concern - User Interface (Adapter)
class ConsoleUI {
public:
ConsoleUI(UserProcessorPort* userProcessor, UserRepositoryPort* userRepository)
: userProcessor_(userProcessor), userRepository_(userRepository) {}
void run() {
std::string name;
std::cout << "Enter your name: ";
std::cin >> name;
std::string processedName = userProcessor_->processName(name);
std::cout << "Processed Name: " << processedName << std::endl;
if (userRepository_->saveUser(processedName)) {
std::cout << "User saved successfully." << std::endl;
} else {
std::cout << "Failed to save user." << std::endl;
}
}
private:
UserProcessorPort* userProcessor_;
UserRepositoryPort* userRepository_;
};
int main() {
UserProcessor processor;
NameRepository repository;
ConsoleUI ui(&processor, &repository);
ui.run();
return 0;
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by isolating the core business logic from external concerns like UI, database, or external services. It achieves this through defining ports (interfaces representing interactions with the outside world) which the core application uses, and adapters (implementations of those ports) translating between the core and the external technologies. This facilitates testability, maintainability, and flexibility, allowing you to swap out implementations without affecting the core logic.
Here, the core logic resides in UseCases. We define ports for input (e.g., IUserPresenter) and output (e.g., IUserRepository). Adapters like ConsolePresenter and InMemoryUserRepository bridge the gap between our application and concrete technologies. Dependency Injection is used to provide these adapters to the core. This leverages C#’s strong typing and composition-focused design to make the architecture clear and maintainable.
// Core Application (Use Cases)
public interface IUserRepository
{
User GetUser(int userId);
void SaveUser(User user);
}
public interface IUserPresenter
{
void PresentUser(User user);
}
public class UserService
{
private readonly IUserRepository _userRepository;
private readonly IUserPresenter _userPresenter;
public UserService(IUserRepository userRepository, IUserPresenter userPresenter)
{
_userRepository = userRepository;
_userPresenter = userPresenter;
}
public void GetUserDetails(int userId)
{
User user = _userRepository.GetUser(userId);
_userPresenter.PresentUser(user);
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// Adapters
public class InMemoryUserRepository : IUserRepository
{
private readonly Dictionary<int, User> _users = new Dictionary<int, User>()
{
{ 1, new User { Id = 1, Name = "Alice" } },
{ 2, new User { Id = 2, Name = "Bob" } }
};
public User GetUser(int userId) => _users.TryGetValue(userId, out var user) ? user : null;
public void SaveUser(User user)
{
_users[user.Id] = user;
}
}
public class ConsolePresenter : IUserPresenter
{
public void PresentUser(User user)
{
Console.WriteLine($"User ID: {user.Id}, Name: {user.Name}");
}
}
// Entry Point (Driving Adapter)
public class Program
{
public static void Main(string[] args)
{
// Composition Root - Injecting Adapters
IUserRepository userRepository = new InMemoryUserRepository();
IUserPresenter userPresenter = new ConsolePresenter();
UserService userService = new UserService(userRepository, userPresenter);
userService.GetUserDetails(1);
}
}
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled, testable, and maintainable software. It achieves this by isolating the application’s core business logic from external concerns like databases, UI, or messaging systems. These external concerns interact with the core through ports – interfaces defining interactions. Adapters implement these ports to connect to specific technologies. This example demonstrates a simple use case: user authentication. The core defines a UserRepository port, and adapters connect to in-memory and (hypothetically) real database implementations. TypeScript’s strong typing and interface support lend themselves beautifully to this pattern, creating clear contracts between the core and adapters.
// Core Application Logic (Independent of external frameworks)
// Define the Port (Interface)
interface UserRepository {
getUser(id: string): User | null;
}
interface AuthenticationService {
authenticate(username: string, password: string): boolean;
}
interface User {
id: string;
username: string;
passwordHash: string;
}
class CoreAuthenticationService implements AuthenticationService {
constructor(private userRepository: UserRepository) {}
authenticate(username: string, password: string): boolean {
const user = this.userRepository.getUser(username);
if (!user) {
return false;
}
// In a real application, use a secure password comparison library
return user.passwordHash === password;
}
}
// Adapters (Connect core to external frameworks/systems)
// In-Memory Adapter (For testing or simple applications)
class InMemoryUserRepository implements UserRepository {
private users: User[] = [
{ id: '1', username: 'user1', passwordHash: 'password1' },
{ id: '2', username: 'user2', passwordHash: 'password2' },
];
getUser(id: string): User | null {
return this.users.find(u => u.username === id);
}
}
// Example Usage (Composition Root)
function runAuthentication() {
const userRepository = new InMemoryUserRepository();
const authenticationService = new CoreAuthenticationService(userRepository);
const isAuthenticated = authenticationService.authenticate('user1', 'password1');
console.log('Authentication result:', isAuthenticated); // Output: true
}
runAuthentication();
// In a more complex application, a database adapter would implement the
// UserRepository interface, connecting to a real database. This allows
// changing the persistence layer without affecting the core logic.
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating the core business logic from external concerns like databases, UI frameworks, or APIs. The core interacts with the outside world only through ports – interfaces defining needed interactions. Adapters sit between the ports and the actual external technologies, translating requests and responses. This allows swapping implementations without modifying the core.
This JavaScript example simulates a user service. The UserService (core) depends on a UserRepository port. Two adapters, InMemoryUserRepository and a stub ExternalUserRepository, implement this port. The ExternalUserRepository represents a dependency (e.g., a database). The core doesn’t know how users are stored, only that a repository can getUser and createUser. A simple CLI interaction is provided via an adapter. It showcases how minimal driving (input) and driven (output) adapters can coexist dynamically. This loosely coupled approach fits JavaScript’s flexibility, using simple interfaces to promote testability and maintainability, and leverages the language’s prevalent use of dependency injection.
// Core Business Logic
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
createUser(name, email) {
if (!name || !email) {
throw new Error("Name and email are required");
}
return this.userRepository.createUser({ name, email });
}
getUser(id) {
return this.userRepository.getUser(id);
}
}
// Port (Interface)
class UserRepository {
getUser(id) {
throw new Error("Method not implemented.");
}
createUser(user) {
throw new Error("Method not implemented.");
}
}
// Adapters (Implementations of the Port)
class InMemoryUserRepository extends UserRepository {
constructor() {
super();
this.users = [];
this.nextId = 1;
}
getUser(id) {
return this.users.find(user => user.id === id);
}
createUser(user) {
const newUser = { ...user, id: this.nextId++ };
this.users.push(newUser);
return newUser;
}
}
class ExternalUserRepository extends UserRepository {
//Simulates interacting with an external system like a database
getUser(id) {
//In a real implementation this would fetch from the database.
return Promise.resolve({ id: id, name: `User ${id}`, email: `user${id}@example.com`});
}
createUser(user) {
//In a real implementation this would save to the database.
return Promise.resolve({...user, id: Math.floor(Math.random() * 1000)});
}
}
// Driving Adapter (CLI Example)
async function main() {
const userRepository = new InMemoryUserRepository(); // or new ExternalUserRepository() for different behaviour
const userService = new UserService(userRepository);
try {
const newUser = await userService.createUser("Alice", "alice@example.com");
console.log("Created user:", newUser);
const retrievedUser = await userService.getUser(newUser.id);
console.log("Retrieved user:", retrievedUser);
} catch (error) {
console.error("Error:", error.message);
}
}
main();
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating core business logic from external concerns like UI, databases, and external services. This is achieved by defining “ports” (interfaces) that the core logic uses to interact with the outside world, and then providing “adapters” that implement these ports for specific technologies. Our example focuses on a simple task list. The core logic (use cases) doesn’t know how tasks are stored; it only knows it can ask a port to add_task or get_tasks. The adapters provide the concrete implementations using, for instance, a memory list or a database. Python’s duck typing and use of interfaces (implicitly defined by abstract methods in abc) make it a natural fit.
from abc import ABC, abstractmethod
# Core Business Logic (Use Cases)
class TaskListService:
def __init__(self, task_port):
self.task_port = task_port
def add_task(self, title):
self.task_port.add_task(title)
def get_tasks(self):
return self.task_port.get_tasks()
# Port (Interface)
class TaskPort(ABC):
@abstractmethod
def add_task(self, title):
pass
@abstractmethod
def get_tasks(self):
pass
# Adapters (Implementations)
# In-Memory Adapter (for testing or simple scenarios)
class InMemoryTaskAdapter(TaskPort):
def __init__(self):
self.tasks = []
def add_task(self, title):
self.tasks.append({"title": title, "done": False})
def get_tasks(self):
return self.tasks
# Example Usage (could be in a CLI, web app, etc.)
if __name__ == "__main__":
task_adapter = InMemoryTaskAdapter()
task_service = TaskListService(task_adapter)
task_service.add_task("Buy groceries")
task_service.add_task("Walk the dog")
tasks = task_service.get_tasks()
print(tasks)
The Hexagonal Architecture (also known as Ports and Adapters) aims to create loosely coupled software by separating core business logic from external concerns like databases, UI, or messaging systems. This separation is achieved through defining ports (interfaces defining how the core interacts with the outside world) and adapters (implementations that connect the core to specific technologies). The core, being agnostic to the implementation details of adapters, is easily testable and adaptable to different infrastructures. This example simulates a simple user service, with ports for user storage and adapters for an in-memory implementation.
// Core Business Logic (Independent of Frameworks/Drivers)
interface UserService {
User getUser(String userId);
void createUser(User user);
}
interface UserRepository {
User findById(String id);
void save(User user);
}
class DefaultUserService implements UserService {
private final UserRepository userRepository;
public DefaultUserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User getUser(String userId) {
return userRepository.findById(userId);
}
@Override
public void createUser(User user) {
userRepository.save(user);
}
}
class User {
String id;
String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() { return id; }
public String getName() { return name; }
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
// Adapters (Implementations for Specific Technologies)
class InMemoryUserRepository implements UserRepository {
private final Map<String, User> users = new HashMap<>();
@Override
public User findById(String id) {
return users.get(id);
}
@Override
public void save(User user) {
users.put(user.getId(), user);
}
}
// Application (Wiring)
public class HexagonalApp {
public static void main(String[] args) {
// Configure Adapters
UserRepository userRepository = new InMemoryUserRepository();
// Wire Core with Adapters
UserService userService = new DefaultUserService(userRepository);
// Use the service
User user1 = new User("123", "Alice");
userService.createUser(user1);
System.out.println(userService.getUser("123"));
}
}