Modern software development often faces one major hurdle: tight coupling. Whether you're swapping out a database, switching from REST to GraphQL, or integrating a new third-party service, making changes without breaking the core business logic can feel like performing surgery with a sledgehammer.
Port and Adapter Architecture, also known as Hexagonal Architecture, was created to solve this exact problem. It puts the core logic at the center and treats all external dependencies—databases, UIs, APIs—as plug-and-play accessories.
In this blog post, we’ll explore this architecture with an intuitive analogy, compare it to traditional layered architecture, and implement a Task Manager App in Java that retrieves tasks for users.
What is Port and Adapter Architecture?
Think of the application core as the brain of your app. It defines what the app does, not how it's done. To interface with the messy outside world, it uses:
Ports: Abstract interfaces that define how external actors interact with the app. Think of ports like the USB ports on your laptop—they stay the same regardless of the device you plug in.
Adapters: Concrete implementations that plug into ports and use specific technologies. Like USB cables, you use different ones depending on the kind of device (application or service) you're connecting.
Configurator: The glue that wires everything together at runtime.
Even if your cable (adapter) changes, your port remains the same. It's this stability that makes the architecture so flexible and powerful.
🏠 Real-World Analogy: Home Appliances
House (Core) = Your application's business logic
Sockets (Ports) = Interfaces that define what’s possible
Appliances (Adapters) = Implementations using those sockets (e.g., toaster, TV)
Electrician (Configurator) = Connects the right appliance to the right socket
Need to switch from a toaster to a microwave? You don’t rebuild the house—you just plug in a new appliance.
🔁 Compared to Layered Architecture
Layered architecture often leads to unnecessary and tight coupling between layers. Typically, applications follow a flow like:
UI → Service → Repository → DB
However, in many real-world cases, the boundaries blur. The business logic starts depending directly on the database entities and infrastructure-specific components. For example, ORM entities like JPA are often available throughout the service and UI layers, exposing technical details such as lazy loading and transaction management to places they shouldn't be.
This not only leads to subtle bugs (like uninitialized collections being accessed from the UI) but also makes it difficult to test business logic in isolation—because it ends up entangled with database access logic.
Worse still, updating infrastructure components—like changing the ORM framework or database version—requires changes across all layers, which becomes a bottleneck and is often neglected due to the high cost.
In contrast, Hexagonal Architecture flips this structure. The application core depends only on ports (interfaces). External systems—whether databases, REST APIs, or UIs—connect via adapters. This keeps the core pure and decoupled from implementation details, enabling isolated testing and easier tech upgrades.. The app core only talks to ports. Everything else plugs in.
🎯 Our Example: Task Manager App
Use Case: "Fetch all pending tasks for a user"
We’ll build it using Port and Adapter architecture. Components:
Port (interface)
Adapter (simulated database)
Application Service (core logic)
Configurator (wiring)
Client (runner)
Step 1: Define the Port
public interface TaskRetrievalPort {
List<Task> getPendingTasksForUser(String userId);
}
This interface represents a contract. The service layer only depends on this.
Step 2: Create the Adapter (Simulating a DB)
public class InMemoryTaskAdapter implements TaskRetrievalPort {
private final Map<String, List<Task>> db = new HashMap<>();
public InMemoryTaskAdapter() {
db.put("user1", List.of(
new Task("1", "Buy milk", true),
new Task("2", "Read book", false)
));
}
@Override
public List<Task> getPendingTasksForUser(String userId) {
return db.getOrDefault(userId, List.of()).stream()
.filter(task -> !task.isCompleted())
.collect(Collectors.toList());
}
}
Step 3: Application Core Service
public class TaskService {
private final TaskRetrievalPort taskPort;
public TaskService(TaskRetrievalPort taskPort) {
this.taskPort = taskPort;
}
public List<Task> getUserPendingTasks(String userId) {
return taskPort.getPendingTasksForUser(userId);
}
}
Step 4: Configurator
public class TaskServiceConfigurator {
public static TaskService createDefaultService() {
TaskRetrievalPort adapter = new InMemoryTaskAdapter();
return new TaskService(adapter);
}
}
Step 5: Task Model + Main Client
public class Task {
private final String id;
private final String description;
private final boolean completed;
public Task(String id, String description, boolean completed) {
this.id = id;
this.description = description;
this.completed = completed;
}
public boolean isCompleted() { return completed; }
public String getDescription() { return description; }
}
public class Main {
public static void main(String[] args) {
TaskService service = TaskServiceConfigurator.createDefaultService();
List<Task> tasks = service.getUserPendingTasks("user1");
tasks.forEach(task -> System.out.println("Pending: " + task.getDescription()));
}
}
✅ Benefits in Practice
Want to add a real DB later? Just create a new adapter.
Need to expose tasks via REST? Add a REST adapter.
Writing tests? Mock the port interface.
No changes to TaskService are needed.
📐 Testing Support
Hexagonal architecture makes testing clean and painless:
Unit tests talk to primary ports.
Mocks can replace secondary ports (e.g., database or notification systems).
Adapters can be tested separately with stubs or integration tools like TestContainers.
🧩 Closing Thoughts
Port and Adapter architecture doesn’t just help you write cleaner code—it empowers you to:
Delay infrastructure decisions
Embrace change confidently
Build with clarity and testability
Start small: wrap your use cases in interfaces, implement them with adapters, and connect them using a configurator. It’s a mindset shift—but one that pays off in the long run.
Top comments (0)