Back to Decomposition & DDD
Q8Question 8 of 9

What is the Hexagonal (Ports and Adapters) architecture, how does it isolate domain logic from infrastructure, and why is it a style you layer on top of microservices or a monolith?

Key Concepts
  • Domain logic sits at the center and depends on nothing external; it never imports Spring, JPA, or HTTP classes.
  • Ports are interfaces owned by the domain: inbound ports (use cases) and outbound ports (repositories, external clients).
  • Adapters are concrete implementations: REST controllers (inbound) and JPA repositories (outbound).
  • Dependency inversion: the domain declares interfaces; adapters implement them; dependencies point inward.
  • The domain can be unit-tested with in-memory fakes, zero database, zero web server.
  • Hexagonal is a code-organization style inside a deployable, not an alternative to microservices or a monolith.
  • Apply it where domain logic is rich (loan approval, settlement); keep trivial CRUD services simpler.

Definition

Put the business rules in the center with technology-agnostic ports, plug concrete adapters onto those ports, and make everything depend inward so you can swap any technology without touching the domain.
  • Domain – entities, value objects, and domain services holding business rules; depends on nothing external.
  • Inbound ports – interfaces defining use cases (e.g., ApproveLoanUseCase); driven by adapters like REST controllers.
  • Outbound ports – interfaces defining what the domain needs (e.g., LoanRepository, CreditBureauPort); implemented by outbound adapters.
  • Adapters – concrete implementations: REST controller (inbound), JPA repository (outbound), HTTP client (outbound).
  • Dependency rule – all dependencies point inward; the domain owns the interfaces; adapters depend on and implement them.
  • Not a deployment choice – hexagonal is a code-organization style applied inside a monolith or inside each microservice.

Picture the loan-approval department at National Bank of India as the heart of the business. Around it are standardized slots (ports): one slot accepts applications whether they come by web, mobile, or phone. Another slot sends records out whether to Oracle or PostgreSQL. The web form, the mobile app, and the Oracle connector are all adapters that plug into these slots. If the bank switches databases, you unplug one adapter and plug in another -- the loan rules in the heart are not touched.

Understanding the Concepts

What is the Hexagonal architecture?

Hexagonal (Ports and Adapters) places domain logic at the center, defines ports (interfaces) for everything it needs or offers, and attaches adapters for specific technologies. The dependency rule is that everything points inward.

How do ports and adapters work?

JAVA
// OUTBOUND PORT: domain declares what it needs
package com.bank.domain.port;

import java.math.BigDecimal;

public interface LoanRepository {
    void save(Loan loan);
    Loan findById(String loanId);
}

public interface CreditBureauPort {
    int fetchScore(String customerId);
}
JAVA
// INBOUND PORT: domain declares what it offers
package com.bank.domain.port;

import java.math.BigDecimal;

public interface ApproveLoanUseCase {
    LoanDecision approve(String customerId, BigDecimal amount);
}
JAVA
// DOMAIN SERVICE: depends only on ports (interfaces)
package com.bank.domain.service;

import com.bank.domain.port.*;
import java.math.BigDecimal;

public class LoanApprovalService implements ApproveLoanUseCase {

    private final CreditBureauPort creditBureau;
    private final LoanRepository loanRepo;

    public LoanApprovalService(CreditBureauPort creditBureau, LoanRepository loanRepo) {
        this.creditBureau = creditBureau;
        this.loanRepo = loanRepo;
    }

    @Override
    public LoanDecision approve(String customerId, BigDecimal amount) {
        int score = creditBureau.fetchScore(customerId);
        if (score < 700) return LoanDecision.rejected("Score below 700");
        Loan loan = new Loan(customerId, amount);
        loanRepo.save(loan);
        return LoanDecision.approved(loan);
    }
}
JAVA
// OUTBOUND ADAPTER: implements the port with real technology
package com.bank.adapter.out;

import com.bank.domain.port.LoanRepository;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaLoanRepository extends JpaRepository<Loan, String>, LoanRepository {
    // Spring Data implements save() and findById() automatically
}
JAVA
// INBOUND ADAPTER: REST controller drives the domain through the inbound port
package com.bank.adapter.in;

import com.bank.domain.port.ApproveLoanUseCase;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;

@RestController
@RequestMapping("/api/loans")
public class LoanController {

    private final ApproveLoanUseCase approveUseCase;

    public LoanController(ApproveLoanUseCase approveUseCase) {
        this.approveUseCase = approveUseCase;
    }

    @PostMapping
    public LoanDecision apply(@RequestParam String customerId,
                              @RequestParam BigDecimal amount) {
        return approveUseCase.approve(customerId, amount);
    }
}

How do you test the domain in isolation?

JAVA
// Unit test with in-memory fakes: zero database, zero Spring
class LoanApprovalServiceTest {
    @Test
    void rejectsLowScore() {
        CreditBureauPort fakeBureau = id -> 600; // always returns 600
        LoanRepository fakeRepo = new InMemoryLoanRepository();
        var service = new LoanApprovalService(fakeBureau, fakeRepo);

        LoanDecision result = service.approve("CUST-1001", new BigDecimal("5000000"));

        assertEquals("REJECTED", result.getStatus());
    }
}
TEXT
Expected output:
Test passed: rejectsLowScore

Why is hexagonal a style layered ON TOP of microservices, not an alternative?

Microservices and monolith are deployment choices (how many units, how they communicate). Hexagonal is about code organization inside one unit. You do not choose between them; you apply hexagonal inside each service.

What are common mistakes?

JAVA
// WRONG: domain imports Spring JPA
package com.bank.domain.service;
import org.springframework.data.jpa.repository.JpaRepository; // BREAKS the architecture

// CORRECT: domain imports only its own port interface
package com.bank.domain.service;
import com.bank.domain.port.LoanRepository; // domain owns this interface

When should you NOT use hexagonal?

For trivial CRUD services where the domain logic is just pass-through, hexagonal adds ceremony without benefit. Apply it where the domain is rich and valuable -- loan approval, settlement, fraud scoring.

What should a senior engineer know?

  • The ports belong to the domain package, which enforces the inward dependency direction at compile time.
  • If your domain imports JPA, Spring, or HTTP classes, you have broken the architecture.
  • You can enforce this with ArchUnit tests in CI.
  • The benefits are testability (test domain with fakes), technology independence (swap DB by writing a new adapter), and clarity (business rules untangled from framework annotations).

How do you enforce the dependency rule with ArchUnit?

ArchUnit tests in CI prevent accidental imports of framework classes into the domain package. If a developer adds a Spring annotation to a domain entity, the build fails.

JAVA
package com.bank.architecture;

import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class HexagonalArchTest {

    // @Test
    public void domainMustNotDependOnSpringOrJpa() {
        var classes = new ClassFileImporter()
            .importPackages("com.bank");

        ArchRule rule = noClasses()
            .that().resideInAPackage("..domain..")
            .should().dependOnClassesThat()
            .resideInAnyPackage(
                "org.springframework..",
                "javax.persistence..",
                "jakarta.persistence..");

        rule.check(classes);
    }
}
TEXT
// Build output when a developer adds @Entity to a domain class:
// ArchUnit test FAILED:
//   com.bank.domain.model.Loan imports jakarta.persistence.Entity
//   Domain must not depend on JPA!
//   Fix: move @Entity to the adapter or use a separate JPA entity in adapter.out

Visual Overview

DIAGRAM
  Hexagonal Architecture

            Inbound Adapters                 Outbound Adapters
  ┌─────────────┐                           ┌─────────────────┐
  │ REST        │──┐                   ┌───►│ JPA Repository  │
  │ Controller  │  │   ┌───────────┐   │    └─────────────────┘
  └─────────────┘  ├──►│           │───┤
  ┌─────────────┐  │   │  DOMAIN   │   │    ┌─────────────────┐
  │ Kafka       │──┘   │           │   └───►│ HTTP Client     │
  │ Listener    │      │ ports =   │        │ (Credit Bureau) │
  └─────────────┘      │ interfaces│        └─────────────────┘
                       └───────────┘
                     Dependencies point INWARD
                     Domain depends on NOTHING external

  Package Structure
  com.bank/
  ├── domain/
  │   ├── model/       (entities, value objects)
  │   ├── port/        (interfaces: inbound + outbound)
  │   └── service/     (domain services)
  ├── adapter/
  │   ├── in/          (REST controller, Kafka listener)
  │   └── out/         (JPA repo, HTTP client)

Key Takeaway

Hexagonal architecture puts the business rules at the center and surrounds them with ports (interfaces) and adapters (concrete technology implementations). The dependency rule is that everything points inward: the domain depends on nothing external and never imports Spring, JPA, or HTTP classes. Inbound adapters like REST controllers drive the domain through inbound ports, and the domain reaches outward through outbound ports implemented by outbound adapters like JPA repositories.

The payoff is testability and technology independence. At National Bank of India, Pankaj could unit-test the loan approval rule with in-memory fakes, no database and no web server, and when the bank migrated its database the team only wrote a new adapter while the approval rule stayed untouched.

The key point is that hexagonal is not an alternative to microservices or a monolith. Microservices and monolith are deployment choices; hexagonal is code organization inside one unit. You apply hexagonal inside each service where the domain logic is rich, and keep trivial CRUD services simpler.

Remember one sentence: put the business rules in the center with technology-agnostic ports, plug concrete adapters onto those ports with dependencies pointing inward, and you can swap any technology by writing a new adapter without touching the domain.

---

Share
Use ← → arrow keys to navigate