It works. Until it doesn't.
As systems scale, teams grow, and complexity creeps in, more senior engineers are switching to something else:
Hexagonal Architecture, also known as Ports and Adapters.
It's not a trend. It's a practical shift born from real pain in production systems.
Let's walk through why.
The Classic Layered Architecture
This is the pattern many of us were taught:
Controller
↓
Service (Business Logic)
↓
Repository (DB Access)
↓
DatabaseIt's easy to set up. It separates concerns. But once real-world complexity hits — background jobs, message queues, third-party APIs, testability — the cracks show.
Where Layered Starts to Break
1. Directional Coupling
Layers are tightly coupled downwards. The service layer must know exactly how the repository works. Testing often requires wiring up every layer below.
2. Hard to Replace Infra
Want to switch from Mongo to Redis for one use case? Good luck — you'll be threading that logic through service and controller layers.
3. Tests Depend on Infra
You either:
- Mock the entire DB layer (not ideal), or
- Spin up real databases in tests (slow and brittle)
4. Business Logic Gets Polluted
In real codebases, business logic ends up mixed with logging, DB calls, HTTP clients, Kafka producers — even retries.
What Hexagonal Architecture Does Differently
Hexagonal says: invert the direction.
Instead of application code depending on infrastructure (DB, APIs), the infrastructure depends on the application.
Business logic lives at the core — pure and testable.
Here's how it looks:
┌────────────────────┐
│ HTTP Handler │
│ CLI Command │
│ Kafka Consumer │
└────────┬───────────┘
│
┌───────▼────────┐
│ Application │
│ (Use Cases) │
└───────┬────────┘
│
┌───────▼────────┐
│ Domain │
│ (Interfaces) │
└───────┬────────┘
│
┌─────────────▼────────────┐
│ Infrastructure Layer │
│ (DB, Redis, APIs, etc.) │
└──────────────────────────┘Folder Structure in Go
payment-service/
│
├── cmd/ # main.go entrypoint
├── internal/
│ ├── app/ # Use cases
│ ├── domain/ # Entities + Interfaces
│ ├── adapters/ # HTTP, Kafka, Mongo, etc.
│ └── config/ # Env loading
└── go.modEach adapter implements an interface from the domain.
Real-World Example: Hexagonal in Go
Interface in Domain
// domain/payment_repository.go
type PaymentRepository interface {
Save(payment *Payment) error
FindByID(id string) (*Payment, error)
}Pure Use Case Logic
// app/payment_service.go
type PaymentService struct {
repo domain.PaymentRepository
}
func (s *PaymentService) Process(payment *domain.Payment) error {
if payment.Amount <= 0 {
return errors.New("invalid amount")
}
return s.repo.Save(payment)
}Mongo Adapter
// adapters/mongo/payment_repo.go
type MongoRepo struct {
collection *mongo.Collection
}
func (m *MongoRepo) Save(p *domain.Payment) error {
_, err := m.collection.InsertOne(context.TODO(), p)
return err
}Testing Becomes Stupidly Simple
Just fake the interface:
type FakeRepo struct {
saved []*domain.Payment
}
func (f *FakeRepo) Save(p *domain.Payment) error {
f.saved = append(f.saved, p)
return nil
}Now you can write unit tests that test only your logic — no database, no HTTP.
Hexagonal Also Supports More Than Just HTTP
You can wire the same business logic to:
- HTTP API
- gRPC service
- Kafka consumer
- CLI tool
- Batch job
All without changing your core logic.
That's massive for microservices where jobs, consumers, and APIs are everywhere.
When Should You Use Hexagonal?
You're working with microservices
You want clean test boundaries
You want to avoid logic duplication
You want to swap DBs or message queues easily
You need to support multiple input/output formats
If you're building a CRUD-only service with no scaling, sure — layered might still be okay.
But once real logic comes in — go hexagonal.
Final Thoughts
Senior engineers don't move away from Layered Architecture because it's wrong. They move because it stops working at scale.
Hexagonal Architecture gives you:
- Clean core logic
- Testable units
- Replaceable infrastructure
- Better long-term maintainability
It's not hype. It's not new. But it works — and that's why more and more experienced Go developers are adopting it.