DEV Community

Cover image for Port and Adapter Architecture in Real Life (with Java)
Bidisha Das
Bidisha Das

Posted on

Port and Adapter Architecture in Real Life (with Java)

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:

  1. Port (interface)

  2. Adapter (simulated database)

  3. Application Service (core logic)

  4. Configurator (wiring)

  5. Client (runner)

Step 1: Define the Port

public interface TaskRetrievalPort {
    List<Task> getPendingTasksForUser(String userId);
}

Enter fullscreen mode Exit fullscreen mode

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());
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Configurator

public class TaskServiceConfigurator {
    public static TaskService createDefaultService() {
        TaskRetrievalPort adapter = new InMemoryTaskAdapter();
        return new TaskService(adapter);
    }
}

Enter fullscreen mode Exit fullscreen mode

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()));
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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)