A minimal Go service that demonstrates Hexagonal Architecture (also known as Ports and Adapters). The application exposes a simple REST API to create and retrieve "items" and keeps the core logic independent of HTTP and storage details.
Hexagonal architecture puts business logic at the centre and defines ports (interfaces) for all input and output. Adapters implement those interfaces, so you can change how users call the app (HTTP, gRPC, CLI) or where data lives (memory, PostgreSQL, APIs) without touching the core.
- Domain – Entities and rules; no dependencies on frameworks or I/O.
- Ports – Interfaces: inbound (how the app is driven) and outbound (what the app needs from the outside).
- Application – Use cases that orchestrate the domain and use outbound ports.
- Adapters – Implementations of ports: inbound adapters call the application; outbound adapters (e.g. repositories) are called by the application.
Dependencies point inward: adapters depend on ports, the application depends on ports and domain, and the domain depends on nothing.
┌─────────────────────────────────────────┐
│ Inbound Adapters │
│ (HTTP, gRPC, CLI, … drive the app) │
└────────────────────┬────────────────────┘
│
▼
┌──────────────┐ ┌─────────────────────────────────────────┐ ┌──────────────┐
│ Outbound │ │ Inbound Ports │ Application │ │ Outbound │
│ Adapters │◄───│ (interfaces) │ (use cases) │───►│ Ports │
│ (DB, APIs…) │ │ │ + Domain │ │ (interfaces) │
└──────────────┘ └─────────────────────────────────────────┘ └──────────────┘
▲
│
┌────────────────────┴────────────────────┐
│ Dependency injection (e.g. in main) │
└─────────────────────────────────────────┘
hexagonal/
├── cmd/
│ └── service/
│ └── main.go # Wiring: builds adapters and app, injects ports
├── internal/
│ ├── domain/ # Core entities and logic (no I/O)
│ │ └── item.go
│ ├── ports/ # Inbound and outbound interfaces
│ │ ├── inbound.go # ItemService – how use cases are invoked
│ │ └── outbound.go # ItemRepository – what the app needs from storage
│ ├── application/ # Use case implementations (depend only on ports + domain)
│ │ └── item_service.go
│ └── adapter/
│ ├── inbound/ # Driving adapters (call into the app)
│ │ └── http/ # REST handler and router
│ └── outbound/ # Driven adapters (implement outbound ports)
│ └── persistence/
│ └── memory.go # In-memory ItemRepository
├── go.mod
└── README.md
| Layer | Role |
|---|---|
| domain | Item entity. Pure business data; no references to HTTP, DB, or frameworks. |
| ports | ItemService (inbound): “create item”, “get by ID”. ItemRepository (outbound): “save”, “find by ID”. |
| application | ItemService implementation: create with ID, persist via ItemRepository; get by ID via repository. Depends only on port interfaces and domain. |
| adapter/inbound/http | HTTP handler: parse request → call ItemService → write response. Depends on inbound port. |
| adapter/outbound/persistence | In-memory map implementing ItemRepository. Can be replaced by a PostgreSQL adapter without changing app or domain. |
| cmd/service | Constructs repository and application, injects them, then starts the HTTP server with the handler. |
go run ./cmd/serviceCreate an item:
curl -X POST http://localhost:8080/items -H "Content-Type: application/json" -d '{"name":"My Item"}'Get by ID (use the id from the create response):
curl http://localhost:8080/items/<id>- New inbound side: Add another adapter (e.g.
internal/adapter/inbound/grpc) that implements the sameports.ItemServicecalls. No changes to domain or application. - New outbound side: Implement
ports.ItemRepositorywith a real DB (e.g.internal/adapter/outbound/persistence/postgres.go) and swap it inmain.go; application and domain stay unchanged. - New use cases: Add methods to the inbound port and implement them in the application layer; then expose them in HTTP (or other) adapters.
This keeps the core testable with mocks (fake repositories, fake services) and keeps technology choices at the edges.