Hexagonal architecture, also called ports and adapters, keeps a service’s core logic distinct from its external concerns. Domain types provide a common language: API handlers and data access code can depend on the domain, while the domain depends on neither. This direction of dependency helps prevent import cycles. section 8.3.1
A small Go service can move toward that design without building every layer:
- Define shared game data in an internal/session package: Game has an ID, attempts left, guesses, and status. Keep business errors, such as a guess after game over, with the domain types. section 8.3.1
- Let HTTP handlers manage request and response details. Put game creation in a separate function, then convert session.Game into the API’s GameResponse. The conversion gives API formatting one clear home and can be tested on its own. section 8.3.2
- Wrap a gRPC client inside internal/client. Its public method returns local habit.Habit values rather than exposing generated protobuf types; an HTTP server can depend on a small HabitsClient interface. section 11.1.3
Listing 8.22 convert.go: Adapter between the API and domain
// ToGameResponse converts a session.Game into an GameResponse.
func ToGameResponse(g session.Game) GameResponse {
apiGame := api.GameResponse{
ID: string(g.ID),
AttemptsLeft: g.AttemptsLeft,
Guesses: make([]Guess, len(g.Guesses)), #1
Status: string(g.Status),
// TODO WordLength #2
}
for index := 0; index < len(g.Guesses); index++ {
apiGame.Guesses[index].Word =
g.Guesses[index].Word #3
apiGame.Guesses[index].Feedback = g.Guesses[index].Feedback
}
if g.AttemptsLeft == 0 { #4
apiGame.Solution = ""// TODO solution #2
}
return apiGame
}
The API adapter copies a session.Game into a GameResponse, including each guess. Its TODOs also show that the conversion is not yet complete. section 8.3.2
Keep generated types at the client boundary
A habits client holds an api.HabitsClient internally. ListHabits calls the generated gRPC method and builds []habit.Habit for callers. Tests supply a mock gRPC client to check both returned habits and errors. The HTTP server receives the wrapper through an interface that names only the operation it needs. section 11.1.3
Use the boundary where it clarifies responsibilities, not as a reason to multiply packages. For the small game service, domain types live in their own package, but domain logic stays in the handlers package; core logic and API details still belong in distinct functions. section 8.3.1 section 8.3.2





