Meet: Your Code in Isolation from the Outside World
\n\nHave you ever rewritten business logic just because you switched your database from PostgreSQL to MongoDB? Or woken up in a cold sweat at the thought of adding a new HTTP endpoint, only to find the entire application is one giant main.go file with 3000 lines?
If so — welcome to the club. If not — you either haven't been writing Go for long, or you're already using Hexagonal Architecture (also known as Ports & Adapters).
\n\nIn this article, we'll break down how to build a Go application that isn't afraid of change. Neither switching databases, nor migrating from REST to gRPC, nor adopting a new framework will force you to rewrite the system's core. Just code. Just Go. No fluff.
\n\nWhat is Hexagonal Architecture in Simple Terms
\n\nImagine your application is a spaceship. At the center is the control room (business logic). Around it are docking ports. Through these ports, you connect different modules: engines (database), sensors (HTTP requests), communication with Earth (message queues).
\n\nIf you decide to replace the engines — you simply undock the old ones and dock new ones. The control room won't even notice the swap. That's Hexagonal Architecture.
\n\nKey concepts:
\n- \n
- Port — an interface that defines how the outside world can interact with the core. For example,
UserRepositorywithSaveandFindByIDmethods. \n - Adapter — a concrete implementation of a port. One port can have many adapters: PostgreSQLAdapter, InMemoryAdapter, MockAdapter for testing. \n
- Core — the business logic, which knows nothing about the outside world. It only works through ports. \n
This approach provides three superpowers: testability (of the core without a database), flexibility (replacing any component), and clear boundaries of responsibility.
\n\nWhy Go is Perfect for Hexagonal Architecture
\n\nGo doesn't enforce OOP, but it provides interfaces — the only thing you need for Ports & Adapters. Interfaces in Go are implicit: a struct automatically satisfies an interface if it implements its methods. This means you can swap an adapter without changing a single line in the core.
\n\nPlus, Go compiles to a binary, making this architecture especially useful for microservices. And the built-in testing and benchmarking allow you to immediately verify component isolation.
\n\nProject Structure: How to Organize Folders
\n\nThere is no single standard, but here is a proven scheme used in production projects:
\n\nmyapp/\n├── cmd/\n│ └── app/\n│ └── main.go # Entry point, dependency wiring\n├── internal/\n│ ├── core/\n│ │ ├── domain/ # Entities, value objects\n│ │ ├── ports/ # Interfaces (ports)\n│ │ └── services/ # Business logic (use cases)\n│ └── adapters/\n│ ├── api/ # HTTP/gRPC adapters\n│ ├── db/ # Repository implementations\n│ └── mq/ # Message queues\n└── go.mod\n\n\nImportant: the internal folder prevents imports from outside — perfect for protecting the core. Adapters live separately and can be easily replaced.
Writing Code: From Interfaces to Implementation
\n\nLet's create a simple service for working with users. We'll start with the core.
\n\nStep 1. Domain and Ports
\n\n// internal/core/domain/user.go\npackage domain\n\nimport "time"\n\ntype User struct {\n ID string\n Email string\n Name string\n CreatedAt time.Time\n}\n\n// internal/core/ports/repositories.go\npackage ports\n\nimport "myapp/internal/core/domain"\n\ntype UserRepository interface {\n Save(user domain.User) error\n FindByID(id string) (domain.User, error)\n FindByEmail(email string) (domain.User, error)\