← Back to the section

Hexagonal architecture has one key rule: the core must not know about infrastructure. It doesn't know whether we use PostgreSQL or MySQL, Sber or some other payment provider, Kafka or RabbitMQ. This lets you swap out infrastructure without touching business logic.

But the core still needs to reach out somewhere — to read and write data, call a payment gateway, publish events. How do you do that without knowing about infrastructure?

The answer is ports. The core describes an interface saying "I need this," and the concrete implementation appears only in the adapter. Dependencies flow from adapters toward the core, not the other way around.

Where a port lives and how it's named

An outbound port is an interface in core/<bc>/port/out/. In core/, not in the adapter. The core declares what it needs; the adapter implements it.

core/src/main/java/<pkg>/
└── domain/
    └── orders/                          # bounded context
        ├── aggregate/Order.java
        └── port/out/                    # ← port interfaces live here
            ├── OrderRepository.java     # working with the aggregate in the DB
            ├── OrderViewRepository.java # read projection for CQRS
            ├── PaymentPort.java         # external payment gateway
            ├── NotificationPort.java    # SMS / email
            └── OrderEventPublisher.java # outgoing events

The naming convention is simple:

What the port doesHow to name itExample
Stores and reads an aggregate<X>RepositoryOrderRepository
Returns read projections (CQRS)<X>ViewRepositoryOrderViewRepository
Calls an external HTTP system<Y>PortPaymentPort, SmsPort
Publishes events directly<Z>EventPublisherOrderEventPublisher

Repository without a Port suffix is a historical convention from DDD — the name speaks for itself. Everything else gets a Port suffix: it's immediately clear this is a contract to an external system.

Port methods work with domain types

This is the key point. A port interface should look like part of the domain — no structures from external SDKs, no entities from the database layer.

// Correct — domain types
public interface PaymentPort {
    RegisterResult register(RegisterCommand cmd);   // RegisterCommand is a domain object
    void cancel(PaymentId paymentId);               // PaymentId is a domain Value Object
}
// Wrong — Sber-specific structures in the core
public interface PaymentPort {
    SberRegisterResponse register(SberRegisterRequest req);
}

What's wrong with the SberRegisterRequest variant:

  • The core now knows about Sber. If tomorrow we switch payment providers, we have to rewrite not just the adapter but everything that uses this interface.
  • Handler tests are forced to create SberRegisterRequest — and that means infrastructure details leaking into clean unit tests.
  • JSON annotations, snake_case fields, and other Sber API details have seeped into the core.

PaymentPort accepts a domain RegisterCommand (fields: amount, orderId, description) and returns a domain RegisterResult (paymentId, redirectUrl). The mapping to Sber API structures lives in SberClientAdapter inside a separate adapter module.

Exception hierarchy

A port is a contract, and exceptions are part of the contract too. Abstract exception classes are declared in core/; concrete subclasses live in the adapters.

// core/domain/orders/port/out/PaymentPortException.java
public abstract class PaymentPortException extends RuntimeException {
    protected PaymentPortException(String msg, Throwable cause) {
        super(msg, cause);
    }
}

// core/domain/orders/port/out/PaymentNotFoundException.java
public class PaymentNotFoundException extends PaymentPortException {
    public PaymentNotFoundException(PaymentId id) {
        super("Payment not found: " + id, null);
    }
}

In the adapter — a concrete exception type tied to the implementation:

// sber-out-adapter/.../SberException.java
public class SberException extends PaymentPortException {
    public SberException(String msg, Throwable cause) { super(msg, cause); }
}

// sber-out-adapter/.../SberClientAdapter.java
@Component
public class SberClientAdapter implements PaymentPort {
    @Override
    public RegisterResult register(RegisterCommand cmd) {
        try {
            return /* ... */;
        } catch (FeignException e) {
            throw new SberException("Failed to register payment", e);
        }
    }
}

The handler in the core catches the domain exception, not SberException:

@UseCaseHandler
public Payment handle(CreatePaymentCommand cmd) {
    try {
        return paymentPort.register(cmd);
    } catch (PaymentDeclinedException e) {
        // handle the decline
    } catch (PaymentPortException e) {
        throw new PaymentSystemUnavailableException(e);
    }
}

If tomorrow you replace SberClientAdapter with an adapter for a different payment provider, the handler doesn't change. It only knows about domain exceptions.

The inbound port is a UseCase

In the classic description of hexagonal architecture there is an inbound port — an interface for entering the core. In UCP no separate interface is needed: the role of the inbound port is played by the UseCase + UseCaseHandler pairing, and the entry point is the UseCaseDispatcher.

// REST controller (in-adapter)
@RestController
public class OrderController implements OrdersApi {
    private final UseCaseDispatcher dispatcher;

    @Override
    public ResponseEntity<OrderJson> createOrder(@Valid CreateOrderRequest req) {
        var cmd = mapper.toCommand(req);
        var order = dispatcher.dispatch(cmd);   // ← entry into the core
        return ResponseEntity.created(...).body(mapper.toJson(order));
    }
}

The controller doesn't depend on a concrete handler. It only knows the UseCaseDispatcher, which routes the command to the right handler by type on its own. This removes unnecessary dependencies and keeps the controller thin.

More on the role of UseCases and handlers — in the Use Case Pattern article.

Common mistakes

The port sits in the adapter module. A port is a contract from the core to infrastructure; it must live in core/. If you put it in the adapter, the core won't see the interface to inject the dependency — the dependency arrow flips the wrong way.

Optional where absence is an error. If the handler expects the object to definitely exist, it's better to throw a domain exception right away than to return an Optional and unwrap it at every call site:

// When absence is a normal case (query)
Optional<Order> findById(OrderId id);

// When absence is an error (command)
Order findRequired(OrderId id);  // throws OrderNotFoundException

Or the handler decides for itself:

Order order = orderRepository.findById(cmd.id())
    .orElseThrow(() -> new OrderNotFoundException(cmd.id()));

A port as an abstract class rather than an interface. A port is a contract, not a structure. An abstract class breaks testability: Mockito creates a mock for an interface almost instantly, but for a class it goes through bytecode manipulation. On top of that, Java has no multiple inheritance of classes: if the adapter already extends something else, it won't be able to implement a port class.

// Correct
public interface PaymentPort { ... }

// Wrong
public abstract class PaymentPort { ... }

In short

  • A port is an interface in core/<bc>/port/out/. The core describes what it needs; the adapter implements it.
  • Names: <X>Repository (aggregate), <X>ViewRepository (CQRS), <Y>Port (external systems), <Z>EventPublisher (events).
  • Port methods accept and return domain types, not structures from external systems' SDKs.
  • Exceptions: abstract classes in core/, concrete subclasses in the adapters. The handler catches the domain exception.
  • Inbound port = UseCase + UseCaseDispatcher. No separate interface is needed.
  • A port is always an interface, not a class: easier to substitute in tests, no restrictions on multiple inheritance.
  • Adapters out — who implements the port interface and how the out-adapter is structured.
  • Adapters in — how a REST controller uses the UseCaseDispatcher as the inbound entry point.
  • Use Case Pattern — about the UseCaseDispatcher and the role of handlers.
  • Repository pattern in jOOQ — a concrete implementation of the <X>Repository port via jOOQ.