Hexagonal Architecture

1. What is Hexagonal Architecture?

Hexagonal Architecture is a design pattern that emphasizes separation of concerns between:

  • Core business logic (the domain)
  • External systems (like databases, web frameworks, APIs, messaging)

It achieves this by introducing:

  • Ports: Interfaces that define what the application can do (input/output)
  • Adapters: Implementations of these interfaces to communicate with external systems

Benefits:

  • Testable core logic without depending on external frameworks
  • Easier to swap out external systems
  • Clear separation between domain and infrastructure

Visual Idea:

        +----------------+
        |   External     |
        |   Systems      |
        +----------------+
        ^       ^       ^
        |       |       |
      Adapter  Adapter  Adapter
        |       |       |
        v       v       v
     +------------------+
     |      Ports       |
     +------------------+
             |
             v
     +------------------+
     |   Application    |
     |     Core         |
     +------------------+

2. Java Example: A Simple User Service

Suppose we want a User Service that can save and retrieve users. We’ll keep the core logic independent of the database.

Step 1: Define the Domain

public class User {
    private String id;
    private String name;

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    // getters
    public String getId() { return id; }
    public String getName() { return name; }
}

Step 2: Define Ports (Interfaces)

// Input port: defines what our application can do
public interface UserService {
    void registerUser(String id, String name);
    User getUser(String id);
}

// Output port: defines what the app needs from external systems
public interface UserRepository {
    void save(User user);
    User findById(String id);
}

Step 3: Implement the Application Logic

public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    // Inject repository (adapter) via constructor
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public void registerUser(String id, String name) {
        // Some domain logic, e.g., validation
        if (id == null || name == null) {
            throw new IllegalArgumentException("ID and Name cannot be null");
        }
        User user = new User(id, name);
        userRepository.save(user); // delegate to adapter
    }

    @Override
    public User getUser(String id) {
        return userRepository.findById(id);
    }
}

Step 4: Implement Adapters (External Systems)

Database Adapter (In-memory example)

import java.util.HashMap;
import java.util.Map;

public class InMemoryUserRepository implements UserRepository {

    private Map<String, User> database = new HashMap<>();

    @Override
    public void save(User user) {
        database.put(user.getId(), user);
    }

    @Override
    public User findById(String id) {
        return database.get(id);
    }
}

Step 5: Wire Everything Together

public class Main {
    public static void main(String[] args) {
        // Create adapter
        UserRepository repository = new InMemoryUserRepository();

        // Inject adapter into core service
        UserService userService = new UserServiceImpl(repository);

        // Use the service
        userService.registerUser("1", "Alice");
        User user = userService.getUser("1");
        System.out.println(user.getName()); // Alice
    }
}

✅ Key Points:

  • UserServiceImpl only depends on interfaces (ports), not concrete implementations.
  • InMemoryUserRepository is an adapter implementing the output port.
  • If tomorrow we switch to a database, we just create a new adapter without touching the core logic.

Leave a Reply