Skip to main content

Hexagonal Architecture (Ports & Adapters Pattern)

Hexagonal Architecture, also known as the Ports and Adapters pattern, is a software design pattern that aims to create a decoupled and maintainable application by separating the core business logic from external concerns (like databases, APIs, and UIs).



Structure of Hexagonal Architecture

A typical Hexagonal Architecture has three main layers:

1️⃣ Core Domain (Application Logic)

  • This contains the business rules and domain models.
  • It is completely independent of external technologies.
  • Example: If you’re building a banking system, this part would include logic for transactions, withdrawals, and deposits.

2️⃣ Ports (Interfaces)

  • These are interfaces that define how the core interacts with external components.
  • Two types of ports:
    • Inbound Ports (driven by external inputs like APIs, UI, or events)
    • Outbound Ports (used to interact with external services like databases, messaging systems, etc.)

3️⃣ Adapters (Implementation of Ports)

  • These are concrete implementations of the ports, responsible for connecting the core with external systems.
  • Examples:
    • UI Adapter (REST API, CLI, Web UI)
    • Persistence Adapter (Database, File System)
    • External Service Adapter (3rd-party API, Payment Gateway)

🟢 Step 1: Define the Core Business Logic (Domain)

This code represents a simple BankAccount class in Java:

public class BankAccount {
    private String accountId;
    private double balance;

    public BankAccount(String accountId, double balance) {
        this.accountId = accountId;
        this.balance = balance;
    }

    public void deposit(double amount) {
        this.balance += amount;
    }

    public void withdraw(double amount) throws Exception {
        if (amount > balance) throw new Exception("Insufficient funds");
        this.balance -= amount;
    }

    public double getBalance() {
        return balance;
    }
}
        

2️⃣ Ports (Interfaces)

Step 2 : These define how the core communicates with external systems.

🟢 Inbound Port (Application Use Cases)

  • This defines the API for external clients (controllers, CLI, etc.).
  • Instead of calling BankAccountService, we rename it to BankAccountUseCase.

// Inbound Port 
public interface BankAccountUseCase {
    void deposit(String accountId, double amount);
    void withdraw(String accountId, double amount) throws Exception;
    double getBalance(String accountId);
}
// Outbound Port (Interface for external database communication)
public interface BankAccountPersistencePort {
    BankAccount findById(String accountId);
    void save(BankAccount account);
}

🟠 Step 3: Implement the Application Service (Core Business Logic)

@Service
public class BankAccountServiceImpl implements BankAccountService {
    private final BankAccountRepository repository;

    public BankAccountServiceImpl(BankAccountRepository repository) {
        this.repository = repository;
    }

    @Override
    public void deposit(String accountId, double amount) {
        BankAccount account = repository.findById(accountId);
        account.deposit(amount);
        repository.save(account);
    }

    @Override
    public void withdraw(String accountId, double amount) throws Exception {
        BankAccount account = repository.findById(accountId);
        account.withdraw(amount);
        repository.save(account);
    }

    @Override
    public double getBalance(String accountId) {
        return repository.findById(accountId).getBalance();
    }
}
    

🟡 Step 3: Define Ports

     

🔵 Step 4: Implement Adapters (Database & REST API)

Adapter 1: Database (Persistence Adapter)

@Repository
public class BankAccountJpaAdapter implements BankAccountPersistencePort {
private final JpaRepository<BankAccount, String> jpaRepository; public BankAccountJpaAdapter(JpaRepository<BankAccount, String> jpaRepository) {
this.jpaRepository = jpaRepository; } @Override public BankAccount findById(String accountId) { return jpaRepository.findById(accountId).orElseThrow(); } @Override public void save(BankAccount account) { jpaRepository.save(account); } }



Advantages of Hexagonal Architecture

Separation of concerns – Business logic is isolated from external dependencies.
Improves testability – Core logic can be tested without DB/API dependencies.
Easier to replace technologies – You can switch databases, frameworks, or APIs without changing core logic.
Supports multiple interfaces – Can be extended with CLI, Web UI, REST API, etc.
Better scalability – Enables independent scaling of different parts of the application.


When to Use Hexagonal Architecture?

✅ When your application needs multiple interfaces (e.g., REST API + CLI + Web UI).
✅ If you plan to switch technologies (e.g., change from SQL to NoSQL).
✅ When maintainability and testability are important.
Not needed for simple applications with no external dependencies.



How does Hexagonal Architecture differ from traditional layered architecture?

Answer: Traditional layered architecture separates concerns into layers like presentation, business logic, and data access. Hexagonal Architecture focuses on separating core business logic from external systems through ports and adapters, providing greater flexibility and decoupling.

How do you test applications built with Hexagonal Architecture?

Answer: Testing applications involves:

  • Unit testing the core business logic by mocking the ports.
  • Integration testing the adapters.
  • End-to-end testing to ensure the entire system works together.

Comments

Popular posts from this blog

Recursion & Choice

Understanding Recursion and Choice Diagrams with Examples Understanding Recursion and Choice Diagrams with Examples Recursion is a powerful concept in programming where a function calls itself to solve smaller instances of the same problem. It's often used in solving complex problems that can be broken down into simpler subproblems. In this blog post, we'll explore the basics of recursion, understand choice diagrams, and see examples to illustrate these concepts. What is Recursion? Recursion occurs when a function calls itself directly or indirectly to solve a problem. A recursive function must have a base case to terminate the recursive calls and prevent infinite recursion. Here's a simple example of a recursive function to calculate the factorial of a number: public class RecursionExample { public static void main(String[] args) { int number = 5; int result = factorial(...

Frameworks

  Communication Frameworks: BLUF:  Google's culture strongly emphasizes efficiency and directness, so getting to the "bottom line up front" is very common. SCQA:  Used in presenting proposals, making recommendations, and structuring project plans. PAS : Used in selling ideas and influencing others. BAB : Used in selling ideas and influencing others. Sparklines : Used in presentation to influence others. STAR:  Widely used in Google's interview process and performance evaluations. Problem-Solving/Decision-Making Frameworks: 5 Whys:  A fundamental technique for root cause analysis, and Google is known for its emphasis on data-driven decision-making, which often involves digging into the root causes of problems. Systems Thinking:  Given the complexity of Google's systems, a systems thinking approach is essential. The Four Questions : Used in post-mortem to review an incident. Human factors : Used in post-mortem to avoid the blame culture. Time Management/Prior...