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.