*June 2025*
**In-ports**
- In-ports give flexibility how can actors request stuff from the application. For example, if stuff is requested via incoming HTTP requests, in-ports provide flexibility to request stuff using incoming Kafka messages, CLI inputs, in-memory job queues, periodic jobs, or with database callbacks.
- Interestingly, in-ports don't necessarily have to be linked to external systems. For example, if a system has an in-memory job queue and it would make sense to trigger the job via a different channel—such as a Kafka message,— an in-port could be useful.
- The adapters triggering the in-ports can interact back (such as HTTP/CLI/NATS responses), but don't have to (such as incoming Kafka messages or periodic jobs).
- Implementation-wise, the incoming adapters don't implement the in-ports , the incoming adapters take the in-port as a parameter (!). That is, the HTTP request handler or the Kafka message handler *is the incoming adapter.* (This took me a while to wrap my head around)
**Out-ports**
- Out-ports give flexibility how the application can request stuff from others or share stuff with others. For example, outgoing HTTP requests, outgoing Kafka messages, writing to db, reading from db, writing to terminal, writing to internal in-memory queues. (Interestingly, a db callback would be behind an in-port, while conventional db reading would be behind an out-port; it's all based on who is the driver.)
- The adapters behind the out-ports can interact back (such as db reads returning data), but don't have to (such as db a callback)
- In language where out-adapter is implemented as a class extending an out-port interface, such as Scala, the out-adapter must support all the operations defined by the out-port. If we need to switch subset of the out-port functionality to a different adaptor, we need to split the out-port. It opens a question, if to define one out-port per one functionality.
**Example in Scala**
```scala
// in-port
trait PaymentsInPort {
def getPayments(customerId: CustomerId): List[Payments]
}
// different adapters using the same in-port
class HttpPaymentsAdapter(port: PaymentsInPort) {
// invoked on http request
def handleRequest(req: HttpRequest) = port.getPayments(...)
}
class KafkaPaymentsAdapter(port: PaymentsInPort) {
// invoked on incoming kafka msg
def processMessage(msg: KafkaMessage) = port.getPayments(...)
}
class CommandLinePaymentsAdapter(port: PaymentsInPort) {
// invoked on user input
def handleCommand(cmd: CliCommand) = port.getPayments(...)
}
class PeriodicPaymentsAdapter(port: PaymentsInPort) {
// invoked periodically, e.g. daily
def run(job: Job) = port.getPayments(...)
}
// out-port
trait CustomerOutPort {
def updateName(customerId: CustomerId, newName: string): Unit
}
// outgoing adapters
class MyCRMAdapter extends CustomerOutPort {
override def updateName(...): // ... call CRM REST API
}
class DbAdapter extends CustomerOutPort {
overrride def updateName(...): // ... store in DB
}
```
**Some views on structuring the codebase**
- Ports are driven by the domain and should not contain adapter-specific models and logic.
- Don't mix adapter-specific stuff and internal domain stuff
- Colocate based on functionality: lower mental load because unrelated features are not visible; less jumping around the codebase, easier to move part of the functionality elsewhere
- Adapters could be made feature-specific, with shared stuff in the shared location
- Subtrees could be validated for architecture, e.g. [ArchUnit](https://www.archunit.org/) for JVM can check that no `domain` code uses adapter's definitions.
```
myservice/
payments/
domain/ <--- contains domain specific stuff, plus in+out ports
incoming/ <--- in adapters
outgoing/ <--- out adapters
admin/
domain/
incoming/
outgoing/
reporting/
domain/
incoming/
outgoing/
shared/
domain/ <--- shared domain stuff
incoming/
outgoing/
```
**Resources**
- Original article - [Hexagonal Architecture - Alistair Cockburn](https://alistair.cockburn.us/hexagonal-architecture)
*Thanks to Pedro, Felicia, and Gerard for the many great discussions. *